Problem
Logging out does not revoke anything server-side. AuthController (backend/src/auth/auth.controller.ts) imports clearAuthCookies from backend/src/auth/helpers/auth-cookies.ts but never calls it, and there is no POST /api/auth/logout route. The frontend authStore.logout() (frontend/lib/store/authStore.ts) only clears localStorage/Zustand state. Meanwhile RefreshTokenRepositoryOperations.revokeAllRefreshTokens(userId) (backend/src/auth/providers/refreshToken.repository.ts) exists and is never invoked anywhere in the codebase.
The SSO logout path is equally incomplete: POST /api/auth/sso/logout (backend/src/auth/sso/sso.controller.ts) runs the SAML strategy only and neither clears the authAccessToken/authRefreshToken cookies nor revokes refresh tokens.
Consequence: after a user clicks "Log out", their refresh token remains valid for the full 7-day lifetime (JWT_REFRESH_EXPIRATION default). On a shared machine, or after a token leak, the session survives the user's explicit logout; there is also no revocation on password change, so a compromised credential set keeps working after a forced reset.
Why this is architecturally hard
- The logout contract must be defined. Should logout revoke only the current refresh-token family (matching the existing rotation/family-revocation machinery in
AuthService.refreshToken) or all sessions (revokeAllRefreshTokens)? Each has different UX and security trade-offs (e.g. "log out everywhere" vs "log out this device").
- Idempotency and cookie handling. The endpoint must be safe to call repeatedly (a second logout is a success, not a 401), clear both auth cookies with the correct paths (
/ and /api/auth/refresh-token per auth-cookies.ts), and work for both the cookie-authenticated SPA and Bearer-token clients.
- The frontend must call it.
authStore.logout() is currently a synchronous local clear; it must become an async call to the new endpoint with the CSRF header (frontend/lib/apiClient.ts already attaches x-csrf-token for state-changing methods) while still clearing local state even if the network call fails.
- Audit and observability hooks already exist but are unused.
AuditAction.LOGOUT and AuditAction.REFRESH_FAMILY_REVOKED are defined in backend/src/audit-log/entities/audit-log.entity.ts, and the security-IP log (SecurityIpLogService) expects security actions — logout must emit them.
Proposed design (not prescriptive)
- Add
POST /api/auth/logout (authenticated) that revokes the presented refresh token (or its family), clears both auth cookies, and writes an AuditAction.LOGOUT entry with the caller's IP.
- Optionally add
POST /api/auth/logout-all using revokeAllRefreshTokens, and revoke all tokens on password reset/change.
- Update
authStore.logout() to call the endpoint (best-effort: clear local state regardless) and clear the SSO session cookie on the SSO path.
Acceptance criteria
Service
Frontend
Tests
Out of scope
SAML SLO redirect flows at the IdP, and token blacklisting for the access token itself (it is short-lived by design).
Getting started
cd backend && npm install && npm run build && npm test
cd ../frontend && npm install && npm run lint
Good first files to read: backend/src/auth/auth.controller.ts, backend/src/auth/helpers/auth-cookies.ts, backend/src/auth/providers/refreshToken.repository.ts (revokeAllRefreshTokens, revokeToken), backend/src/audit-log/entities/audit-log.entity.ts (AuditAction.LOGOUT), and frontend/lib/store/authStore.ts (logout).
Complexity: High — 6 pts
Problem
Logging out does not revoke anything server-side.
AuthController(backend/src/auth/auth.controller.ts) importsclearAuthCookiesfrombackend/src/auth/helpers/auth-cookies.tsbut never calls it, and there is noPOST /api/auth/logoutroute. The frontendauthStore.logout()(frontend/lib/store/authStore.ts) only clearslocalStorage/Zustand state. MeanwhileRefreshTokenRepositoryOperations.revokeAllRefreshTokens(userId)(backend/src/auth/providers/refreshToken.repository.ts) exists and is never invoked anywhere in the codebase.The SSO logout path is equally incomplete:
POST /api/auth/sso/logout(backend/src/auth/sso/sso.controller.ts) runs the SAML strategy only and neither clears theauthAccessToken/authRefreshTokencookies nor revokes refresh tokens.Consequence: after a user clicks "Log out", their refresh token remains valid for the full 7-day lifetime (
JWT_REFRESH_EXPIRATIONdefault). On a shared machine, or after a token leak, the session survives the user's explicit logout; there is also no revocation on password change, so a compromised credential set keeps working after a forced reset.Why this is architecturally hard
AuthService.refreshToken) or all sessions (revokeAllRefreshTokens)? Each has different UX and security trade-offs (e.g. "log out everywhere" vs "log out this device")./and/api/auth/refresh-tokenperauth-cookies.ts), and work for both the cookie-authenticated SPA and Bearer-token clients.authStore.logout()is currently a synchronous local clear; it must become an async call to the new endpoint with the CSRF header (frontend/lib/apiClient.tsalready attachesx-csrf-tokenfor state-changing methods) while still clearing local state even if the network call fails.AuditAction.LOGOUTandAuditAction.REFRESH_FAMILY_REVOKEDare defined inbackend/src/audit-log/entities/audit-log.entity.ts, and the security-IP log (SecurityIpLogService) expects security actions — logout must emit them.Proposed design (not prescriptive)
POST /api/auth/logout(authenticated) that revokes the presented refresh token (or its family), clears both auth cookies, and writes anAuditAction.LOGOUTentry with the caller's IP.POST /api/auth/logout-allusingrevokeAllRefreshTokens, and revoke all tokens on password reset/change.authStore.logout()to call the endpoint (best-effort: clear local state regardless) and clear the SSO session cookie on the SSO path.Acceptance criteria
Service
POST /api/api/auth/logout(final path under the/apiprefix) revokes the refresh token server-side, clearsauthAccessTokenandauthRefreshTokencookies, is idempotent (repeat calls return success), and writes an audit-log entry with the actor's IP.POST /api/auth/refresh-token, and password reset/change revokes all of the account's refresh tokens.POST /api/auth/sso/logoutalso clears the NovaLabs auth cookies and revokes the SSO user's refresh tokens.Frontend
authStore.logout()calls the logout endpoint (with CSRF header) and clears local state even if the request fails.Tests
Out of scope
SAML SLO redirect flows at the IdP, and token blacklisting for the access token itself (it is short-lived by design).
Getting started
Good first files to read:
backend/src/auth/auth.controller.ts,backend/src/auth/helpers/auth-cookies.ts,backend/src/auth/providers/refreshToken.repository.ts(revokeAllRefreshTokens,revokeToken),backend/src/audit-log/entities/audit-log.entity.ts(AuditAction.LOGOUT), andfrontend/lib/store/authStore.ts(logout).Complexity: High — 6 pts