feat(auth): implement JWT logout with token revocation#58
Conversation
- add jti claim to issued JWTs for unique token identification - introduce revoked_tokens table via migration - implement store methods for token revocation - add POST /api/v1/auth/logout endpoint - update requireAuth middleware to reject revoked tokens - add TokenChecker and TokenRevoker interfaces - extend authentication tests to cover logout and revoked token scenarios
|
While working on the authentication flow I noticed that the system currently supports login and logout, but there does not appear to be an API endpoint for creating users. Would it make sense to add a user creation or signup endpoint in the future (for example Also regarding user creation, would you prefer a single endpoint where the role (e.g. Another observation I had while reviewing the authentication flow: would introducing a Happy to implement this in a follow-up PR if it aligns with the design. |
|
While reviewing the auth implementation again, I noticed that auth.go was still using the standard log package while most of the codebase uses log/slog for structured logging. I pushed a small refactor commit to align the auth logging with the structured logging approach, following the logging consistency discussion in #52. |
aaronbrethorst
left a comment
There was a problem hiding this comment.
Chaitanya, solid approach to token revocation here. The jti claim + database blocklist is the standard pattern for JWT logout, and you've wired it through cleanly — the TokenChecker and TokenRevoker interfaces keep the middleware testable, the ON CONFLICT DO NOTHING in RevokeToken makes double-logout idempotent, and the TestLoginLogoutRevokeFlow integration test walks through the full lifecycle convincingly. The log.Printf → slog cleanup is a nice bonus.
Critical
- Migration number conflict (
migrations/000006_add_revoked_tokens.{up,down}.sql). The branch is based on an oldermainthat only had migrations 000001–000003. Since then,000004_add_vehicle_received_indexhas landed onmain. Your migration jumps straight to 000006, skipping 000004 and 000005.golang-migraterequires sequential numbering with no gaps. Please rebase on the latestmainand renumber your migration to000005.
Important
-
No mechanism to clean up expired revoked tokens (
migrations/000006_add_revoked_tokens.up.sql,store_revocation.go). Therevoked_tokenstable stores anexpires_atcolumn but nothing ever deletes rows past their expiry. Since every logout adds a row that's never removed, this table will grow indefinitely. You don't need to solve this in this PR, but theexpires_atcolumn is inert right now — please add a code comment instore_revocation.gonoting that a periodic cleanup job (e.g.DELETE FROM revoked_tokens WHERE expires_at < NOW()) is needed as a follow-up. -
Revocation check silently skipped for tokens without
jti(auth.go:162). If thejticlaim is missing or empty, theifblock at line 162 is skipped entirely and the request proceeds. This is fine for backwards compatibility with pre-existing tokens, but it means any token issued without ajti(e.g. by a test, a script, or a future code path that forgets to include it) can never be revoked. Please add a comment explaining that this is intentional for backwards compatibility with tokens issued before this change.
Suggestions
- Consider
204 No Contentfor logout (auth.go:208). The current200 OKwith{"message": "logged out successfully"}works, but logout is a void operation — the client doesn't need a response body.204 No Contentis the more conventional choice and saves the client from parsing a response it doesn't use. Not a blocker.
Strengths
- Clean interface design:
TokenCheckerandTokenRevokerare minimal single-purpose interfaces that compose well with the existingUserFetcherpattern. - Thorough test suite: The
TestLoginLogoutRevokeFlowtest is particularly good — it walks through valid-token → logout → rejected-token in sequence, exactly how a real client would use this. - Idempotent revocation:
ON CONFLICT (jti) DO NOTHINGmeans double-logout doesn't error, which is the right behavior. - Structured logging migration: Replacing
log.Printfwithslog.Error/slog.Warnbrings the auth code in line with the rest of the codebase.
Recommended Action
Request changes. Rebase on latest main and renumber migration to 000005. Add the comments noted in items 2 and 3.
|
Thanks for the feedback, I’m working on the migration renumbering and the improvements suggested above. I had one quick follow-up: would you be open to adding a username field (unique, alongside email) for login in a future PR? I was thinking this could help reduce login friction a bit, especially in cases where users might not remember the exact email they used.From an implementation side, we could simply restrict usernames from containing @, so we can clearly decide whether to query by email or username. That way we still keep lookups efficient (indexed, O(log n)) and avoid OR-based queries. It should also stay predictable from a security perspective since both fields would remain unique and unambiguous. Happy to go with whatever direction you think fits best. |
|
no, just email. please fix the merge conflicts when you have a chance. |
This PR adds logout support to the JWT authentication system by introducing token revocation. Building on the authentication foundation from PR #29, this update ensures tokens can be invalidated immediately upon logout rather than remaining active for their full 24-hour lifespan.
To achieve this, a jti claim was added to all issued JWTs, and a revoked_tokens database migration was introduced to act as a server-side blocklist. A new POST /api/v1/auth/logout endpoint handles the revocation, supported by new TokenChecker and TokenRevoker interfaces. The existing requireAuth middleware was also updated to intercept and reject any tokens found on this blocklist.
Test cases were also added to cover successful logouts and ensure revoked tokens are properly rejected.