refactor: replace Bearer token auth with HttpOnly cookies
All checks were successful
check / check (push) Successful in 2m21s
All checks were successful
check / check (push) Successful in 2m21s
- Remove POST /api/v1/register endpoint entirely - Session creation (POST /api/v1/session) now sets neoirc_auth HttpOnly cookie instead of returning token in JSON body - Login (POST /api/v1/login) now sets neoirc_auth HttpOnly cookie instead of returning token in JSON body - Add PASS IRC command for setting session password (enables multi-client login via POST /api/v1/login) - All per-request auth reads from neoirc_auth cookie instead of Authorization: Bearer header - Cookie properties: HttpOnly, SameSite=Strict, Secure when behind TLS - Logout and QUIT clear the auth cookie - Update CORS to AllowCredentials:true with origin reflection - Remove Authorization from CORS AllowedHeaders - Update CLI client to use cookie jar (net/http/cookiejar) - Remove Token field from SessionResponse - Add SetPassword to DB layer, remove RegisterUser - Comprehensive test updates for cookie-based auth - Add tests: TestPassCommand, TestPassCommandShortPassword, TestPassCommandEmpty, TestSessionCookie - Update README extensively: auth model, API reference, curl examples, security model, design principles, roadmap closes #83
This commit is contained in:
@@ -5,120 +5,11 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
||||
)
|
||||
|
||||
const minPasswordLength = 8
|
||||
|
||||
// HandleRegister creates a new user with a password.
|
||||
func (hdlr *Handlers) HandleRegister() http.HandlerFunc {
|
||||
return func(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
) {
|
||||
request.Body = http.MaxBytesReader(
|
||||
writer, request.Body, hdlr.maxBodySize(),
|
||||
)
|
||||
|
||||
hdlr.handleRegister(writer, request)
|
||||
}
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) handleRegister(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
) {
|
||||
type registerRequest struct {
|
||||
Nick string `json:"nick"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
var payload registerRequest
|
||||
|
||||
err := json.NewDecoder(request.Body).Decode(&payload)
|
||||
if err != nil {
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"invalid request body",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
payload.Nick = strings.TrimSpace(payload.Nick)
|
||||
|
||||
if !validNickRe.MatchString(payload.Nick) {
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"invalid nick format",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(payload.Password) < minPasswordLength {
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"password must be at least 8 characters",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
sessionID, clientID, token, err :=
|
||||
hdlr.params.Database.RegisterUser(
|
||||
request.Context(),
|
||||
payload.Nick,
|
||||
payload.Password,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.handleRegisterError(
|
||||
writer, request, err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.stats.IncrSessions()
|
||||
hdlr.stats.IncrConnections()
|
||||
|
||||
hdlr.deliverMOTD(request, clientID, sessionID, payload.Nick)
|
||||
|
||||
hdlr.respondJSON(writer, request, map[string]any{
|
||||
"id": sessionID,
|
||||
"nick": payload.Nick,
|
||||
"token": token,
|
||||
}, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) handleRegisterError(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
err error,
|
||||
) {
|
||||
if db.IsUniqueConstraintError(err) {
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"nick already taken",
|
||||
http.StatusConflict,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.log.Error(
|
||||
"register user failed", "error", err,
|
||||
)
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"internal error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
// HandleLogin authenticates a user with nick and password.
|
||||
func (hdlr *Handlers) HandleLogin() http.HandlerFunc {
|
||||
return func(
|
||||
@@ -195,9 +86,66 @@ func (hdlr *Handlers) handleLogin(
|
||||
request, clientID, sessionID, payload.Nick,
|
||||
)
|
||||
|
||||
hdlr.setAuthCookie(writer, request, token)
|
||||
|
||||
hdlr.respondJSON(writer, request, map[string]any{
|
||||
"id": sessionID,
|
||||
"nick": payload.Nick,
|
||||
"token": token,
|
||||
"id": sessionID,
|
||||
"nick": payload.Nick,
|
||||
}, http.StatusOK)
|
||||
}
|
||||
|
||||
// handlePass handles the IRC PASS command to set a
|
||||
// password on the authenticated session, enabling
|
||||
// multi-client login via POST /api/v1/login.
|
||||
func (hdlr *Handlers) handlePass(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
lines := bodyLines()
|
||||
if len(lines) == 0 || lines[0] == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrNeedMoreParams, nick,
|
||||
[]string{irc.CmdPass},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
password := lines[0]
|
||||
|
||||
if len(password) < minPasswordLength {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
irc.ErrNeedMoreParams, nick,
|
||||
[]string{irc.CmdPass},
|
||||
"Password must be at least 8 characters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
err := hdlr.params.Database.SetPassword(
|
||||
request.Context(), sessionID, password,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"set password failed", "error", err,
|
||||
)
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"internal error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user