-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
apiaudio-mediabugSomething isn't workingSomething isn't workingci-cddependenciesemailenhancementNew feature or requestNew feature or requestperformancequestionFurther information is requestedFurther information is requestedsecurity
Description
Feature: Implement POST /api/v1/auth/logout — Session Termination Endpoint
Problem
Without a logout endpoint, a user's session can only be terminated by waiting for both the access token and refresh token to expire naturally. This is unacceptable for a secure application — users must be able to explicitly end their session, especially on shared devices. Additionally, simply deleting the cookie client-side is insufficient: the refresh token jti remains valid in Redis and the access token remains usable until its expiry.
Proposed Solution
Implement POST /api/v1/auth/logout which performs a two-step server-side invalidation:
- Blacklist the Access Token: The AT's
jtiis written to a Redis blacklist with a TTL equal to its remaining lifetime. Theget_current_userdependency checks this blacklist on every authenticated request. - Revoke the Refresh Token: The RT's
jtiis deleted fromrefresh_token_storein Redis, making further token rotations impossible.
Finally, the server clears the HttpOnly refresh token cookie by overwriting it with an expired one.
User Stories
- As a user, I want to log out and have my session immediately invalidated on the server, so that even if someone intercepts my access token it cannot be used after I log out.
- As a user on a shared device, I want to log out and be confident that no one can resume my session using the refresh token cookie, even before it expires.
- As a developer, I want logout to succeed even if the client sends an expired or missing refresh token, so the user is never stuck in a state where they cannot log out.
Acceptance Criteria
POST /api/v1/auth/logoutrequires a valid access token in theAuthorization: Bearer <token>header.- Access Token Blacklisting:
- The AT's
jtiis extracted from the token payload. - It is written to Redis as
blacklist:{jti}with a TTL equal to the token's remaining lifetime in seconds. - From this point, the
get_current_userdependency rejects thisjtiwith401 Unauthorizedon any subsequent request.
- The AT's
- Refresh Token Revocation:
- The RT
jtiis read from theHttpOnlycookie (if present) and deleted from Redis (refresh_token:{jti}). - If the cookie is absent or already revoked, logout still succeeds — this case is not treated as an error.
- The RT
- Cookie Clearance: The server overwrites the
refresh_tokencookie with an empty value andMax-Age=0to instruct the browser to delete it immediately:Set-Cookie: refresh_token=; HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth; Max-Age=0 - On success, the response is
200 OK:{ "status": "ok", "message": "Successfully logged out." } - If the access token is expired or invalid, return
401 Unauthorized— the client should redirect to login. - The endpoint is rate-limited to 20 requests/minute per IP.
- Unit and integration tests cover: successful logout (both tokens revoked), logout with missing RT cookie (AT still blacklisted), and subsequent request with blacklisted AT jti returning
401.
Proposed Technical Details
- Router:
app/api/v1/endpoints/auth.py— newPOST /logoutroute. - Authentication: The route uses the standard
get_current_userdependency to validate and decode the AT. The decodedTokenData(carryingjtiand remaining TTL) is passed to the logout logic. - AT Blacklist in
app/services/token_store.py:blacklist_access_token(jti: str, ttl_seconds: int)— setsblacklist:{jti}with TTL.is_access_token_blacklisted(jti: str) -> bool— checks key existence.
get_current_userupdate inapp/core/deps.py:- After decoding a valid JWT, call
is_access_token_blacklisted(jti). IfTrue, raiseUnauthorizedException(code="TOKEN_REVOKED").
- After decoding a valid JWT, call
- Cookie Clear:
response.delete_cookie("refresh_token", path="/api/v1/auth")in the route handler. - AT Remaining TTL: Computed as
token_exp - int(datetime.utcnow().timestamp())to set the exact Redis TTL, so the blacklist entry self-cleans when the token would have expired anyway. - New/Modified Files:
app/api/v1/endpoints/auth.py— addPOST /logout[MODIFY]app/services/token_store.py— addblacklist_access_token,is_access_token_blacklisted[MODIFY]app/core/deps.py— add blacklist check inget_current_user[MODIFY]
Tasks
- Implement
blacklist_access_tokenandis_access_token_blacklistedinapp/services/token_store.py. - Update
get_current_userinapp/core/deps.pyto check the AT blacklist on every authenticated request. - Implement
POST /api/v1/auth/logoutinapp/api/v1/endpoints/auth.py. - Revoke the refresh token
jtifrom Redis during logout (gracefully handle missing cookie). - Clear the
HttpOnlycookie by settingMax-Age=0in the logout response. - Apply
@limiter.limit("20/minute")rate limit to the logout route. - Write unit tests for
blacklist_access_token,is_access_token_blacklisted, and the updatedget_current_user. - Write integration tests: successful logout, logout with no RT cookie, subsequent request with blacklisted AT returning
401.
Open Questions/Considerations
- Should we support a "logout from all devices" variant (e.g.,
POST /logout?all=true) that callsrevoke_all_user_tokens(email)and blacklists all known ATs for the user? - The AT blacklist only covers the remaining
expwindow. IfACCESS_TOKEN_EXPIRE_MINUTESis very long (e.g., 60 min), the Redis blacklist entry lives for that full duration. Is this an acceptable trade-off, or should we shorten the AT lifetime? - Should the logout endpoint be exposed to unauthenticated clients (no AT required) so that a client with only an expired AT can still clear its refresh token cookie server-side?
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
apiaudio-mediabugSomething isn't workingSomething isn't workingci-cddependenciesemailenhancementNew feature or requestNew feature or requestperformancequestionFurther information is requestedFurther information is requestedsecurity