Skip to content

feat(auth): add JWT token revocation with Redis blocklist#476

Open
antharya05 wants to merge 1 commit into
Dev-Card:mainfrom
antharya05:feat/jwt-token-revocation-clean
Open

feat(auth): add JWT token revocation with Redis blocklist#476
antharya05 wants to merge 1 commit into
Dev-Card:mainfrom
antharya05:feat/jwt-token-revocation-clean

Conversation

@antharya05
Copy link
Copy Markdown

@antharya05 antharya05 commented Jun 4, 2026

Summary

Implements secure JWT session invalidation using a Redis-backed blocklist.

Previously, JWTs remained valid until expiration even after logout, meaning a stolen token could continue accessing protected APIs after a user logged out. This change introduces server-side token revocation so tokens are immediately invalidated after logout while maintaining backward compatibility with existing logout flows.

Closes #306


Type of Change

  • Bug fix
  • New feature
  • Refactor (no functional change)
  • UI / Design change
  • Tests only
  • Documentation
  • Infrastructure / DevOps
  • Security

What Changed

  • Added shared JWT utilities in src/utils/jwt.ts:

    • extractRawJwt() to consistently extract tokens from Authorization headers or cookies
    • blocklistKey() to generate SHA-256 hashed Redis blocklist keys
  • Extended the global authentication decorator in app.ts:

    • Checks Redis blocklist before jwtVerify()
    • Rejects revoked tokens immediately
    • Registers @fastify/cookie before @fastify/jwt so cookie-based authentication works correctly for browser clients
  • Added authenticated DELETE /auth/logout endpoint:

    • Requires a valid JWT
    • Stores a hash of the token signature in Redis
    • Uses TTL equal to the token's remaining lifetime
    • Automatically self-cleans when the token expires
  • Simplified legacy POST /auth/logout:

    • Preserved for backward compatibility
    • Clears cookies only
    • No longer writes unverified tokens to Redis
  • Added comprehensive logout test coverage:

    • Revocation flow
    • Cookie authentication
    • Redis failures
    • Edge cases
    • End-to-end revocation behavior

How to Test

  1. Start the backend and Redis.
npm install
npm run dev
  1. Generate a JWT using an authenticated flow and access a protected endpoint.

  2. Revoke the token:

DELETE /auth/logout
Authorization: Bearer <token>
  1. Retry the same protected request using the revoked token.

Expected result:

401 Unauthorized
  1. Run the logout test suite:
npm test -- logout

Expected result:

  • All logout/revocation tests pass.

Checklist

  • My code follows the project's coding style.
  • TypeScript compiles without errors.
  • I have added or updated tests for the changes I made.
  • All relevant tests pass locally.
  • I have updated documentation where necessary.
  • No new console.log or debug statements left in the code.
  • Breaking changes are documented in this PR description.

Additional Context

Security decisions:

  • Redis blocklist entries store a SHA-256 hash of the JWT signature rather than the raw token.
  • Blocklist entries expire automatically using the JWT's remaining lifetime.
  • Redis failures intentionally fail open to avoid taking down authenticated requests during Redis outages; token expiration remains the fallback protection mechanism.
  • POST /auth/logout no longer writes unverified tokens to Redis, preventing abuse of the logout endpoint.

Repository note:

While validating this feature, I compared the latest main branch against this branch.

Existing failures in event.test.ts and team.test.ts occur identically on main and were not introduced by this authentication change.

This PR introduces no additional test regressions.

Adds secure logout that revokes the current JWT by storing a hash of its
signature in Redis with a TTL equal to the token's remaining lifetime.
The entry self-cleans when the JWT naturally expires, keeping Redis lean.

Changes:
- utils/jwt.ts: extractRawJwt() and blocklistKey(SHA-256(sig)) utilities
- app.ts:       authenticate decorator checks Redis blocklist before jwtVerify;
                registers @fastify/cookie before @fastify/jwt so cookie-based
                auth works for web browser clients (was silently broken before)
- routes/auth.ts: DELETE /auth/logout endpoint (requires valid JWT);
                  POST /auth/logout simplified to cookie-clear only (backward compat)
- logout.test.ts: 36 tests covering revocation flow, cookie auth, Redis failures,
                  edge cases, and end-to-end invariants
- app.test.ts:  set JWT_SECRET/ENCRYPTION_KEY fallbacks so CI can call buildApp()
- package.json: add typecheck script consumed by CI workflow
- ciScript.js:  fix path generation — test files in __tests__/ were being
                double-suffixed (logout.test.ts -> logout.test.test.ts)

Security decisions documented inline:
- Fail-open on Redis outage (acceptable for a portfolio app; JWT expiry is backup)
- SHA-256 hash of signature as blocklist key (claims never stored in Redis)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 4, 2026

@antharya05 is attempting to deploy a commit to the Prashantkumar Khatri's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

CI — Checks Failed

Backend — FAIL

Check Result
Lint PASS
Test PASS
Typecheck FAIL

Mobile — SKIP

Check Result
Lint -
Test -

Web — SKIP

Check Result
Check -
Build -

Last updated: Thu, 04 Jun 2026 19:00:12 GMT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue Template: Insecure Session Invalidation via Redis JWT Blocklist

1 participant