Skip to content

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

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

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

Conversation

@antharya05
Copy link
Copy Markdown

@antharya05 antharya05 commented May 31, 2026

Summary

Implements secure JWT session invalidation using Redis blocklisting.

Previously, JWTs remained valid until expiration even after logout, meaning a stolen token could continue accessing protected APIs after a user logged out. This PR introduces server-side token revocation support so tokens are immediately invalidated after logout.

Closes #306


Type of Change

  • New feature
  • Security
  • Tests only

What Changed

  • Added protected DELETE /auth/logout endpoint for secure token revocation
  • Added Redis-backed JWT blocklist support with automatic TTL cleanup
  • Added shared JWT utility helpers in src/utils/jwt.ts
  • Extended global authenticate middleware to reject revoked tokens before jwtVerify()
  • Added logout/session revocation tests in src/__tests__/logout.test.ts
  • Preserved compatibility with existing OAuth/auth flows during rebase conflict resolution
  • Added hashed token signature storage instead of storing raw JWTs directly

How to Test

  1. Start Docker services and backend:
docker compose up -d
cd apps/backend
npx tsx src/server.ts
  1. Generate a JWT token:
$response = Invoke-RestMethod `
  -Method POST `
  -Uri http://localhost:3000/auth/dev-login `
  -ContentType "application/json" `
  -Body "{}"

$token = $response.token
  1. Verify authenticated access works:
Invoke-RestMethod `
  -Uri http://localhost:3000/api/profiles/me `
  -Headers @{ Authorization = "Bearer $token" }
  1. Logout and revoke the token:
Invoke-RestMethod `
  -Method DELETE `
  -Uri http://localhost:3000/auth/logout `
  -Headers @{ Authorization = "Bearer $token" }
  1. Retry the same request using the same token:
Invoke-RestMethod `
  -Uri http://localhost:3000/api/profiles/me `
  -Headers @{ Authorization = "Bearer $token" }

Expected result:

  • Request fails with 401 Unauthorized
  1. Run logout tests:
cd apps/backend
pnpm vitest run src/__tests__/logout.test.ts

Checklist

  • My code follows the project's coding style (pnpm -r run lint passes).
  • TypeScript compiles without errors (pnpm -r run typecheck).
  • I have added or updated tests for the changes I made.
  • All tests pass locally (pnpm -r run test).
  • 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.

Screenshots / Recordings

Attached:

  • Successful authenticated request before logout
  • Successful logout response
  • Revoked token returning 401 Unauthorized
  • Passing logout test suite
Screenshot 2026-05-31 151441 Screenshot 2026-05-31 152147

Additional Context

  • Redis blocklist entries use TTL equal to remaining JWT lifetime, so entries self-expire automatically.
  • Redis stores hashed token signatures instead of raw JWTs for improved security.
  • Redis failures intentionally use a fail-open strategy to avoid taking down all authenticated requests during Redis outages.
  • Existing unrelated failing tests in event.test.ts were already present in the repository and are unrelated to this implementation.

@antharya05
Copy link
Copy Markdown
Author

hey @Harxhit , @ShantKhatri , please look into this PR, thank you.

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