Technical design — MOD-044 JWT role-based access control¶
Module: MOD-044 — JWT role-based access control
System: SD07
Repo: bank-platform
FR scope: FR-273, FR-274, FR-275, FR-276, FR-458, FR-459, FR-460, FR-461
Policies satisfied: DT-001 (GATE), GOV-007 (AUTO), GOV-006 (LOG)
Author: Sub-agent (completed by orchestrator verification)
Date: 2026-04-22
Dependencies: MOD-045 (Built), MOD-104 (Built)
Deployed: yes — SST permalink https://sst.dev/u/e0493176
Objective¶
Issues, validates, revokes, and logs JWTs for every inter-module and API call in the bank platform. Signs RS256 with kid-indexed keys sourced from MOD-045's Secrets Manager namespace; exposes a Lambda authoriser that any API Gateway or service can invoke to enforce role → scope policy. Revocation list + refresh-rotation add defence against token theft.
Test evidence¶
AWS_PROFILE=bank-dev STAGE=dev pnpm test → 35 / 35 passing (26 unit + 9 integration).
| FR / Policy | Pass |
|---|---|
| FR-273 issue token with required claims | ✓ |
| FR-274 validate signature / expiry / issuer / revoke | ✓ |
| FR-275 kid-based key lookup + rotation grace window | ✓ |
| FR-276 audit log immutable + 7y retention | ✓ |
| FR-458 role → scope mapping | ✓ |
| FR-459 scope gate before handler | ✓ |
| FR-460 revocation list enforced | ✓ |
| FR-461 refresh token rotation | ✓ |
| DT-001 GATE signing key not readable by non-MOD-044 roles | ✓ (negative) |
| GOV-007 AUTO every decision logged | ✓ |
| GOV-006 LOG audit immutability | ✓ |
Stacks¶
MOD-044-jwt-rbac/src/stacks/:
- signing-keys.ts — Active + previous signing key secrets in Secrets Manager (kid-indexed). MOD-045 rotation Lambda hooked for 90-day rotation.
- authoriser-lambda.ts + issuer-lambda.ts + revoker-lambda.ts — the three Lambda handlers backing the JWT flows.
- revocation-list.ts — DynamoDB table with TTL on expiry timestamp.
- audit-log.ts — Dedicated CloudWatch log group with 7y retention + IAM-level deny on UPDATE/DELETE for every runtime role.
SSM outputs table¶
| Path | Value | Consumer |
|---|---|---|
/bank/{env}/mod044/authoriser/arn |
Lambda ARN | API Gateway authoriser / cross-module Lambda invoke |
/bank/{env}/mod044/issuer/arn |
Lambda ARN | POST /auth/token + POST /auth/refresh invokers |
/bank/{env}/mod044/revoker/arn |
Lambda ARN | Ops + MOD-046 PAM session revocation |
/bank/{env}/mod044/signing-key/current-kid |
String — current kid | Token issuers |
/bank/{env}/mod044/signing-key/previous-kid |
String — previous kid (grace window) | Token validators |
/bank/{env}/mod044/revocation-list/table-name |
DynamoDB table name | Revoker + validator Lambdas |
/bank/{env}/mod044/audit-log/group-arn |
Log group ARN | Audit exporters, MOD-076 subscription filters |
See MOD-044-jwt-rbac/src/outputs.ts for the complete list.
Key decisions¶
- RS256 only. No HS256 — prevents key-confusion attacks.
- Kid-based rotation grace window. Previous key stays valid for 24h after rotation so in-flight tokens remain accepted.
- Revocation list TTL = token exp. DynamoDB removes entries when underlying tokens expire anyway, keeping the list bounded.
- Refresh token rotation is server-side. Client presents old refresh → server issues new access + new refresh, invalidates old refresh. Reuse of old refresh → treat as compromise, revoke entire family.
Operational notes¶
- Deploy:
AWS_PROFILE=bank-dev pnpm sst deploy --stage <env>(fromMOD-044-jwt-rbac/) - Tests:
AWS_PROFILE=bank-dev STAGE=dev pnpm test - Audit log bucket is 7y retained —
aws logs delete-log-groupis blocked by the IAM immutability policy; if teardown is required, the orchestrator break-glass role must detach the policy first
Related¶
- Handoff:
docs/handoffs/MOD-044-complete.handoff.md - MOD-045 rotation Lambda: see MOD-045 outputs
- MOD-104 Cognito pools + Lambda roles: see MOD-104 outputs