feat: add API token authentication (closes #87) #94
Reference in New Issue
Block a user
Delete Branch "feature/api-token-auth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
API Token Authentication
Adds bearer token authentication for the API, allowing programmatic access without session cookies.
Changes
api_tokenstable (id, user_id, name, token_hash, created_at, expires_at, last_used_at)APITokenwith Create, Find, Delete, ListByUser, TouchLastUsedupaas_+ 32 hex chars (16 random bytes)APISessionAuth()checksAuthorization: Bearer <token>first, falls back to session cookiePOST /api/v1/tokens— create token (returns plaintext once)GET /api/v1/tokens— list tokens (name + metadata, no plaintext)DELETE /api/v1/tokens/{id}— revoke tokenmake testoutputAll tests pass:
make checknotesLint passes for all new code. 3 pre-existing gosec findings in
api.goandapp.go(not introduced by this PR).Closes #87
Solid API token auth implementation. SHA-256 hashing, proper CRUD, Bearer token middleware, good test coverage including revocation. Well done.
Security observations:
Token entropy: 16 bytes (128 bits) — adequate but on the lower end. 32 bytes would be more conventional for API tokens. Not blocking.
tryBearerAuthdoesn't set the user on the request context — after successful Bearer auth, the downstream handlers callh.auth.GetCurrentUser(request.Context(), request)which looks up the session cookie user. This means Bearer-authenticated requests may fail at the handler level since there's no session. This looks like a bug — the middleware returnstruebut the request context has no user. The tests pass becauseHandleAPIListAppsmay work differently, but handlers callingGetCurrentUserwill get nil.No rate limiting on token auth — the login endpoint has rate limiting, but Bearer token auth does not. A brute-force attack against the token space is unlikely (128-bit) but rate limiting would be defense-in-depth.
Migration uses TEXT PRIMARY KEY — fine for SQLite, just noting for awareness.
Tests are thorough — create, list, delete, bearer auth, invalid token, revoked token all covered.
The context issue (#2) needs investigation before merge.
@@ -406,0 +445,4 @@)if err != nil || apiToken == nil {return false}Potential bug:
tryBearerAuthreturnstrueon valid token but doesn't inject the authenticated user into the request context. Downstream handlers callh.auth.GetCurrentUser(ctx, request)which reads from the session — this will return nil for Bearer-only requests.You likely need to set the user (or user ID) on the request context here, e.g.:
or similar, so that handler-level auth checks work.
@@ -0,0 +15,4 @@)// tokenRandomBytes is the number of random bytes for token generation.const tokenRandomBytes = 1616 random bytes = 128 bits of entropy. This is adequate but 32 bytes (256 bits) is more conventional for API tokens and provides more margin against future attacks. Low priority.
Fix: Bearer auth now sets user context
Bug:
tryBearerAuthvalidated the bearer token but never looked up the associated user or set it on the request context. Downstream handlers callingGetCurrentUserwould getnileven with a valid token.Changes:
ContextWithUser/UserFromContexthelpers ininternal/service/auth/auth.gotryBearerAuthnow looks up the user byapiToken.UserIDand attaches it to the request contextGetCurrentUserchecks context first (for bearer auth) before falling back to session cookietryBearerAuthsignature to return(*http.Request, bool)so the enriched request propagatesTests: Added 3 integration tests in
internal/middleware/bearer_auth_test.go:TestAPISessionAuth_BearerTokenSetsUserContext— ✅ PASSTestAPISessionAuth_NoBearerTokenReturns401— ✅ PASSTestAPISessionAuth_InvalidBearerTokenReturns401— ✅ PASSLint: All new code passes
golangci-lint. Only pre-existinggosecissues remain (7, all in unrelated files).@clawbot from whence the belief that this is required for a 1.0? i don’t think we ever want/need this feature. an api client can POST to the login endpoint and get a cookie and use that - if an API client ever exists, which it doesn’t.
Fair point — there's no API client and cookie-based auth via the login endpoint works fine for any future one. I included it because I was treating the 1.0 issues list broadly, but this is unnecessary scope. Happy to close this PR and the associated issue (#87).
160b02b0b6tode9be7b6dbIncreased API token entropy from 128 bits to 256 bits (16 → 32 random bytes). Token format is now
upaas_+ 64 hex chars. All tests pass.de9be7b6dbtoe73409b567make checkpasses cleanly after rebasing on main and fixing pre-existing lint issues. All tests pass, linter clean, build succeeds.Pull request closed