diff --git a/docs/ERROR_CODE_REGISTRY.md b/docs/ERROR_CODE_REGISTRY.md index f0fabd1..1b827d6 100644 --- a/docs/ERROR_CODE_REGISTRY.md +++ b/docs/ERROR_CODE_REGISTRY.md @@ -284,6 +284,55 @@ Error codes are defined in `src/constants/error.constants.ts` and should be trea } ``` +--- +### TOKEN_EXPIRY_TAMPERED + +**HTTP Status:** 401 Unauthorized + +**Meaning:** JWT token expiry has been tampered with. The token's `exp` claim does not match the expected `iat + ttlSeconds`. + +**When to use:** + +- Token's `exp` is greater than `iat + ttlSeconds` (expiry was extended beyond the valid window) +- Token's `exp` is less than `iat + ttlSeconds` (expiry was shortened, token revoked early) + +**Example:** + +```json +{ + "success": false, + "error": { + "code": "TOKEN_EXPIRY_TAMPERED", + "message": "Token expiry has been tampered with" + } +} +``` + +--- + +### MISSING_IAT + +**HTTP Status:** 401 Unauthorized + +**Meaning:** JWT token is missing the `iat` (issued-at) claim. The token cannot have its expiry validated without an issuance timestamp. + +**When to use:** + +- Token does not include an `iat` claim +- Token payload is decoded but `iat` is undefined + +**Example:** + +```json +{ + "success": false, + "error": { + "code": "MISSING_IAT", + "message": "Token is missing the issued-at (iat) claim" + } +} +``` + --- ## Adding New Error Codes diff --git a/src/constants/error.constants.ts b/src/constants/error.constants.ts index c207198..f50622c 100644 --- a/src/constants/error.constants.ts +++ b/src/constants/error.constants.ts @@ -15,6 +15,8 @@ export const ErrorCode = { JWT_ERROR: 'TOKEN_ERROR', INSUFFICIENT_BALANCE: 'insufficient_balance', NOT_A_CREATOR: 'not_a_creator', + TOKEN_EXPIRY_TAMPERED: 'token_expiry_tampered', + MISSING_IAT: 'missing_iat', } as const; export type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode]; diff --git a/src/utils/token-expiry.validator.test.ts b/src/utils/token-expiry.validator.test.ts new file mode 100644 index 0000000..1aaa69f --- /dev/null +++ b/src/utils/token-expiry.validator.test.ts @@ -0,0 +1,53 @@ +import { validateTokenExpiry } from '../utils/token-expiry.validator'; + +describe('validateTokenExpiry', () => { + it('accepts a token where exp === iat + ttl', () => { + const payload = { + iat: 1000, + exp: 1060, + ttlSeconds: 60, + }; + + const result = validateTokenExpiry(payload); + + expect(result.valid).toBe(true); + }); + + it('rejects a token where exp > iat + ttl with token_expiry_tampered', () => { + const payload = { + iat: 1000, + exp: 1100, + ttlSeconds: 60, + }; + + const result = validateTokenExpiry(payload); + + expect(result.valid).toBe(false); + expect(result.code).toBe('token_expiry_tampered'); + }); + + it('rejects a token where exp < iat + ttl with token_expiry_tampered', () => { + const payload = { + iat: 1000, + exp: 1050, + ttlSeconds: 60, + }; + + const result = validateTokenExpiry(payload); + + expect(result.valid).toBe(false); + expect(result.code).toBe('token_expiry_tampered'); + }); + + it('rejects a token with no iat claim with missing_iat', () => { + const payload = { + exp: 1100, + ttlSeconds: 60, + }; + + const result = validateTokenExpiry(payload); + + expect(result.valid).toBe(false); + expect(result.code).toBe('missing_iat'); + }); +}); \ No newline at end of file diff --git a/src/utils/token-expiry.validator.ts b/src/utils/token-expiry.validator.ts new file mode 100644 index 0000000..2b44242 --- /dev/null +++ b/src/utils/token-expiry.validator.ts @@ -0,0 +1,35 @@ +/** + * Token expiry validator. + * + * Computes the expected expiry as iat + ttlSeconds and validates that + * the token's exp claim matches. Returns specific error codes for + * tampered expiry or missing iat claim. + */ +export interface TokenExpiryValidationResult { + valid: boolean; + code?: string; +} + +export function validateTokenExpiry( + payload: { iat?: number; exp: number; ttlSeconds: number } +): TokenExpiryValidationResult { + if (payload.iat === undefined) { + return { valid: false, code: 'missing_iat' }; + } + + const expectedExp = payload.iat + payload.ttlSeconds; + + if (payload.exp === expectedExp) { + return { valid: true }; + } + + if (payload.exp > expectedExp) { + return { valid: false, code: 'token_expiry_tampered' }; + } + + if (payload.exp < expectedExp) { + return { valid: false, code: 'token_expiry_tampered' }; + } + + return { valid: false }; +} \ No newline at end of file