01.What JWTs Actually Give You
A JWT proves that a token was signed by your server. That's it. It does not mean the user is still authorized. It does not mean the session hasn't been revoked. It does not mean the user hasn't been compromised. Treating JWTs as sufficient authentication is one of the most common backend security mistakes.
02.The Revocation Problem
JWTs are stateless by design — once issued, they're valid until expiry. If a user logs out, changes their password, or gets compromised, you cannot invalidate their existing token without a blocklist. Store a Redis-backed token blocklist keyed by jti (JWT ID) with TTL matching the token expiry.
async revokeToken(jti: string, expiresAt: number) {
const ttl = expiresAt - Math.floor(Date.now() / 1000);
await this.redis.setex(`blocklist:${jti}`, ttl, '1');
}
async isRevoked(jti: string): Promise<boolean> {
return !!(await this.redis.get(`blocklist:${jti}`));
}03.Refresh Token Rotation
Short-lived access tokens (15 minutes) + long-lived refresh tokens (7 days) is the right model. On every refresh, issue a new refresh token and invalidate the old one. Detect token reuse — if an already-used refresh token is presented, it indicates a compromised session. Revoke the entire family immediately.
04.RBAC Beyond Simple Role Checks
Role-based access control should be permission-based, not role-based. A user has a role, a role has permissions. Check permissions, not roles. This gives you granular control without rewriting guards every time you add a feature.
- ▸Never check: user.role === 'admin'
- ▸Always check: user.permissions.includes('documents:read')
- ▸Store permission sets in Redis, refresh on login
- ▸Audit every permission check for sensitive resources
05.Audit Logging
Every sensitive action — login, logout, permission change, resource access — must be logged with timestamp, user ID, IP address, and action type. Store these in an append-only table or stream to CloudWatch. This is non-negotiable for any production system handling user data.