refactor: replace Bearer token auth with HttpOnly cookies (#84)
All checks were successful
check / check (push) Successful in 2m34s
All checks were successful
check / check (push) Successful in 2m34s
## Summary
Major auth refactor replacing Bearer token authentication with HttpOnly cookie-based auth, removing the registration endpoint, and adding the PASS IRC command for password management.
## Changes
### Removed
- `POST /api/v1/register` endpoint (no separate registration path)
- `RegisterUser` DB method
- `Authorization: Bearer` header parsing
- `token` field from all JSON response bodies
- `Token` field from CLI `SessionResponse` type
### Added
- **Cookie-based authentication**: `neoirc_auth` HttpOnly cookie set on session creation and login
- **PASS IRC command**: set a password on the authenticated session via `POST /api/v1/messages {"command":"PASS","body":["password"]}` (minimum 8 characters)
- `SetPassword` DB method (bcrypt hashing)
- Cookie helpers: `setAuthCookie()`, `clearAuthCookie()`
- Cookie properties: HttpOnly, SameSite=Strict, Secure when behind TLS, Path=/
- CORS updated: `AllowCredentials: true` with origin reflection function
### Auth Flow
1. `POST /api/v1/session {"nick":"alice"}` → sets `neoirc_auth` cookie, returns `{"id":1,"nick":"alice"}`
2. (Optional) `POST /api/v1/messages {"command":"PASS","body":["s3cret"]}` → sets password for multi-client
3. Another client: `POST /api/v1/login {"nick":"alice","password":"s3cret"}` → sets `neoirc_auth` cookie
4. Logout and QUIT clear the cookie
### Tests
- All existing tests updated to use cookies instead of Bearer tokens
- New tests: `TestPassCommand`, `TestPassCommandShortPassword`, `TestPassCommandEmpty`, `TestSessionCookie`
- Register tests removed
- Login tests updated to use session creation + PASS command flow
### README
- Extensively updated: auth model documentation, API reference, curl examples, security model, design principles, roadmap
- All Bearer token references replaced with cookie-based auth
- Register endpoint documentation removed
- PASS command documented
### CI
- `docker build .` passes (format check, lint, all tests, build)
closes #83
Co-authored-by: clawbot <clawbot@eeqj.de>
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #84
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #84.
This commit is contained in:
@@ -44,6 +44,7 @@ const (
|
||||
defaultMaxBodySize = 4096
|
||||
defaultHistLimit = 50
|
||||
maxHistLimit = 500
|
||||
authCookieName = "neoirc_auth"
|
||||
)
|
||||
|
||||
func (hdlr *Handlers) maxBodySize() int64 {
|
||||
@@ -103,23 +104,18 @@ func resolveHostname(
|
||||
return strings.TrimSuffix(names[0], ".")
|
||||
}
|
||||
|
||||
// authSession extracts the session from the client token.
|
||||
// authSession extracts the session from the auth cookie.
|
||||
func (hdlr *Handlers) authSession(
|
||||
request *http.Request,
|
||||
) (int64, int64, string, error) {
|
||||
auth := request.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
return 0, 0, "", errUnauthorized
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(auth, "Bearer ")
|
||||
if token == "" {
|
||||
cookie, err := request.Cookie(authCookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
return 0, 0, "", errUnauthorized
|
||||
}
|
||||
|
||||
sessionID, clientID, nick, err :=
|
||||
hdlr.params.Database.GetSessionByToken(
|
||||
request.Context(), token,
|
||||
request.Context(), cookie.Value,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, 0, "", fmt.Errorf("auth: %w", err)
|
||||
@@ -128,6 +124,46 @@ func (hdlr *Handlers) authSession(
|
||||
return sessionID, clientID, nick, nil
|
||||
}
|
||||
|
||||
// setAuthCookie sets the authentication cookie on the
|
||||
// response.
|
||||
func (hdlr *Handlers) setAuthCookie(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
token string,
|
||||
) {
|
||||
secure := request.TLS != nil ||
|
||||
request.Header.Get("X-Forwarded-Proto") == "https"
|
||||
|
||||
http.SetCookie(writer, &http.Cookie{ //nolint:exhaustruct // optional fields
|
||||
Name: authCookieName,
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
}
|
||||
|
||||
// clearAuthCookie removes the authentication cookie from
|
||||
// the client.
|
||||
func (hdlr *Handlers) clearAuthCookie(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
) {
|
||||
secure := request.TLS != nil ||
|
||||
request.Header.Get("X-Forwarded-Proto") == "https"
|
||||
|
||||
http.SetCookie(writer, &http.Cookie{ //nolint:exhaustruct // optional fields
|
||||
Name: authCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
MaxAge: -1,
|
||||
})
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) requireAuth(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
@@ -295,10 +331,11 @@ func (hdlr *Handlers) executeCreateSession(
|
||||
|
||||
hdlr.deliverMOTD(request, clientID, sessionID, nick)
|
||||
|
||||
hdlr.setAuthCookie(writer, request, token)
|
||||
|
||||
hdlr.respondJSON(writer, request, map[string]any{
|
||||
"id": sessionID,
|
||||
"nick": nick,
|
||||
"token": token,
|
||||
"id": sessionID,
|
||||
"nick": nick,
|
||||
}, http.StatusCreated)
|
||||
}
|
||||
|
||||
@@ -1003,6 +1040,11 @@ func (hdlr *Handlers) dispatchCommand(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, bodyLines,
|
||||
)
|
||||
case irc.CmdPass:
|
||||
hdlr.handlePass(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, bodyLines,
|
||||
)
|
||||
case irc.CmdTopic:
|
||||
hdlr.handleTopic(
|
||||
writer, request,
|
||||
@@ -2138,6 +2180,8 @@ func (hdlr *Handlers) handleQuit(
|
||||
request.Context(), sessionID,
|
||||
)
|
||||
|
||||
hdlr.clearAuthCookie(writer, request)
|
||||
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "quit"},
|
||||
http.StatusOK)
|
||||
@@ -3059,6 +3103,8 @@ func (hdlr *Handlers) HandleLogout() http.HandlerFunc {
|
||||
)
|
||||
}
|
||||
|
||||
hdlr.clearAuthCookie(writer, request)
|
||||
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
|
||||
Reference in New Issue
Block a user