srs-api/src/services/token.ts
Aswin Biju 61514fb82f
All checks were successful
deploy / test (push) Successful in 1m33s
deploy / deploy (push) Successful in 18s
feat(users): added a wizard to reset a password and supporting services for the srs PWA
2026-11-04 20:44:36 +05:30

43 lines
1.6 KiB
TypeScript

import { createHmac, timingSafeEqual } from 'node:crypto';
export const TOKEN_LIFETIME_SECONDS = 12 * 60 * 60;
export interface TokenService {
issue(userId: number): string;
verify(token: string): number | undefined;
}
const b64 = (data: string | Buffer): string => Buffer.from(data).toString('base64url');
const HEADER = b64(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
export function createTokenService(secret: string, now: () => Date = () => new Date()): TokenService {
const sign = (data: string): Buffer => createHmac('sha256', secret).update(data).digest();
return {
issue(userId) {
const iat = Math.floor(now().getTime() / 1000);
const payload = b64(JSON.stringify({ sub: userId, iat, exp: iat + TOKEN_LIFETIME_SECONDS }));
return `${HEADER}.${payload}.${b64(sign(`${HEADER}.${payload}`))}`;
},
verify(token) {
const parts = token.split('.');
if (parts.length !== 3 || parts[0] !== HEADER) return undefined;
const [header, payload, signature] = parts as [string, string, string];
const expected = sign(`${header}.${payload}`);
const given = Buffer.from(signature, 'base64url');
if (given.length !== expected.length || !timingSafeEqual(given, expected)) return undefined;
try {
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as { sub?: unknown; exp?: unknown };
const nowSeconds = Math.floor(now().getTime() / 1000);
if (!Number.isInteger(claims.sub) || typeof claims.exp !== 'number' || claims.exp <= nowSeconds) return undefined;
return claims.sub as number;
} catch {
return undefined;
}
},
};
}