Skip to content

Add JWT RS256 key rotation support#1834

Open
Killerjunior wants to merge 1 commit into
EarnQuestOne:mainfrom
Killerjunior:feat/1704-jwt-key-rotation
Open

Add JWT RS256 key rotation support#1834
Killerjunior wants to merge 1 commit into
EarnQuestOne:mainfrom
Killerjunior:feat/1704-jwt-key-rotation

Conversation

@Killerjunior

Copy link
Copy Markdown

Linked Issue

Closes #1704


Description

Implemented JWT RS256 key loading from environment variables and added support for key rotation without downtime by allowing verification against multiple public keys.

What changed?

  • Added shared JWT key utilities:
    • BackEnd/src/common/utils/jwt-keys.ts
  • Updated auth JWT strategy to support multiple verification keys during rotation:
    • BackEnd/src/modules/auth/strategies/jwt.strategy.ts
  • Updated websocket auth guard to verify against multiple public keys:
    • BackEnd/src/common/guards/ws-auth.guard.ts
  • Updated JWT signing modules to load private key via env helper:
    • BackEnd/src/modules/auth/auth.module.ts
    • BackEnd/src/modules/websocket/websocket.module.ts
  • Updated backend env example with RS256 key env vars and rotation example:
    • BackEnd/.env.example
  • Added backend key rotation documentation and linked it from backend docs:
    • docs/backend/jwt-key-rotation.md
    • docs/backend/README.md

Why was it changed?

  • To remove reliance on file-based PEM keys in deployments.
  • To support safe JWT key rotation where old tokens remain valid during the transition window.
  • To document an operational rotation runbook for production teams.

How was it implemented?

  • Introduced centralized key parsing/normalization helpers for JWT_PRIVATE_KEY, JWT_PUBLIC_KEY, and JWT_PUBLIC_KEYS.
  • Verification paths now attempt validation using each configured public key (rotation window support).
  • Signing paths use only the active private key from env.
  • Added docs for zero-downtime rotation: deploy new signing key + dual public verification keys, wait out token lifetime, then retire old key.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to break)
  • Security fix
  • Refactor (no functional change)
  • Documentation update
  • Configuration / DevOps change
  • Tests only

Contract Changelog Discipline

  • No contract implementation changes - not applicable
  • Updated contracts/earn-quest/CHANGELOG.md under ## [Unreleased]
  • If breaking, added a ### Breaking Changes entry with impact, affected files, and migration steps
  • If breaking, used Conventional Commit breaking metadata (type(scope)!:) in the PR title or commit history
  • If breaking, included a BREAKING CHANGE: explanation below

BREAKING CHANGE details (required for breaking contract changes):

BREAKING CHANGE:

Test Evidence

Unit Tests

  • New unit tests added for changed logic
  • All existing unit tests pass (npm run test)
  • Coverage does not regress (npm run test:cov)

Test output / screenshot:

Not run in this change set.

E2E / Integration Tests

  • E2E tests added or updated (npm run test:e2e)
  • Tested manually against a local environment

Swagger / API Documentation

  • No API changes - Swagger update not applicable
  • New endpoints documented with @ApiOperation, @ApiResponse, and @ApiBearerAuth decorators
  • Updated DTOs annotated with @ApiProperty / @ApiPropertyOptional
  • Swagger UI verified locally at /api/docs and responses are accurate
  • Breaking changes to existing contracts are documented in the description above

Error Handling Checklist

HTTP Exceptions

  • Appropriate NestJS HTTP exceptions used (NotFoundException, BadRequestException, ForbiddenException, UnauthorizedException, ConflictException, etc.)
  • No raw Error thrown where an HTTP exception is expected
  • Global exception filter handles all unhandled errors gracefully
  • Error responses follow the project's standard error shape

Input Validation (DTOs)

  • All incoming request bodies and query params have a corresponding DTO
  • DTOs use class-validator decorators (@IsString, @IsUUID, @IsNotEmpty, @IsOptional, etc.)
  • class-transformer decorators applied where necessary (@Transform, @Type, @Expose)
  • ValidationPipe is applied globally or at the controller level - raw unvalidated input is never used

Guards & Authorization

  • Endpoints requiring authentication are protected with @UseGuards(JwtAuthGuard) or equivalent
  • Admin-only endpoints use the appropriate admin guard / role check
  • Public endpoints are explicitly marked with @Public() decorator where applicable
  • Throttler guard behaviour verified - rate limits are not unintentionally bypassed

Logging

  • Significant operations and state transitions are logged using the project's Winston logger (LoggerService)
  • Errors are logged at error level with stack traces
  • No sensitive data (passwords, secrets, private keys, tokens) is included in log output
  • Incoming request / response logging is handled by the global LoggerMiddleware - no duplicate logs added

Stellar / Soroban Contract Interactions

  • Contract calls wrapped in try/catch with descriptive error messages
  • Horizon / Soroban RPC failures do not crash the service - fallback or retry logic applied where appropriate
  • Transaction signing uses environment-provided keys only - no hardcoded secrets

Database / Migration

  • No database changes - not applicable
  • TypeORM migration created and tested (npm run typeorm:generate-migration)
  • Migration is reversible (down migration implemented)
  • Seed data updated if required (seed.ts)

Breaking Type / Model Changes (Frontend — FE-068)

  • My PR touches none of the watched type/model paths — not applicable.
  • I classified my change as: ☐ breaking-typesbreaking-runtimeaddedchangeddeprecatedremovedfixedsecurity
  • I added a bullet to ## [Unreleased] in FrontEnd/my-app/CHANGELOG.md OR a new file in FrontEnd/my-app/.changeset/.
  • If breaking, my entry includes a before/after Migration: code block.
  • cd FrontEnd/my-app && npm run changelog:check passes locally.
  • If I am asserting this change is non-breaking despite touching a watched file, I added the changelog-skip label or [changelog-skip] to the PR title.

Final Pre-Merge Checklist

  • Branch is up to date with main / master
  • Linting passes (npm run lint)
  • Formatting passes (npm run format)
  • No console.log / debug statements left in production code
  • No hardcoded secrets, API keys, or environment-specific values in source code
  • .env.example updated if new environment variables were introduced
  • ReadMe Backend.md or ReadMe Frontend.md updated if setup steps changed
  • Self-review completed - I have read through every line of the diff

Additional Notes for Reviewer

  • Rotation compatibility is provided via JWT_PUBLIC_KEYS (comma-separated list).
  • If JWT_PUBLIC_KEYS is absent, verification falls back to JWT_PUBLIC_KEY.
  • New tokens are signed only with JWT_PRIVATE_KEY (active key).
  • Recommended rollout is documented in docs/backend/jwt-key-rotation.md.

@drips-wave

drips-wave Bot commented Jun 29, 2026

Copy link
Copy Markdown

@Killerjunior Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

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.

Add API Key Rotation Support for JWT RS256 Keys

1 participant