← back to Wine Finder Next
lib/jwtAuth.ts
197 lines
// JWT Authentication
import crypto from 'crypto';
interface JWTPayload {
sub: string; // subject (user ID)
email: string;
role: string;
iat: number; // issued at
exp: number; // expiration
jti?: string; // JWT ID (for revocation)
}
interface JWTHeader {
alg: string;
typ: string;
}
const JWT_CONFIG = {
algorithm: 'HS256',
expiresIn: 24 * 60 * 60, // 24 hours in seconds
issuer: 'wine-dao-platform',
audience: 'wine-dao-users'
};
// Get JWT secret
function getJWTSecret(): string {
return process.env.JWT_SECRET || 'change-me-in-production-use-256-bit-key';
}
// Base64 URL encode
function base64urlEncode(str: string): string {
return Buffer.from(str)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Base64 URL decode
function base64urlDecode(str: string): string {
const padding = '='.repeat((4 - (str.length % 4)) % 4);
const base64 = str.replace(/-/g, '+').replace(/_/g, '/') + padding;
return Buffer.from(base64, 'base64').toString('utf8');
}
// Create JWT token
export function createJWT(userId: string, email: string, role: string): string {
const now = Math.floor(Date.now() / 1000);
const header: JWTHeader = {
alg: JWT_CONFIG.algorithm,
typ: 'JWT'
};
const payload: JWTPayload = {
sub: userId,
email,
role,
iat: now,
exp: now + JWT_CONFIG.expiresIn,
jti: crypto.randomBytes(16).toString('hex')
};
const encodedHeader = base64urlEncode(JSON.stringify(header));
const encodedPayload = base64urlEncode(JSON.stringify(payload));
const signature = crypto
.createHmac('sha256', getJWTSecret())
.update(`${encodedHeader}.${encodedPayload}`)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return `${encodedHeader}.${encodedPayload}.${signature}`;
}
// Verify JWT token
export function verifyJWT(token: string): { valid: boolean; payload?: JWTPayload; error?: string } {
try {
const parts = token.split('.');
if (parts.length !== 3) {
return { valid: false, error: 'Invalid token format' };
}
const [encodedHeader, encodedPayload, signature] = parts;
// Verify signature
const expectedSignature = crypto
.createHmac('sha256', getJWTSecret())
.update(`${encodedHeader}.${encodedPayload}`)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
if (signature !== expectedSignature) {
return { valid: false, error: 'Invalid signature' };
}
// Decode payload
const payload: JWTPayload = JSON.parse(base64urlDecode(encodedPayload));
// Check expiration
const now = Math.floor(Date.now() / 1000);
if (payload.exp && payload.exp < now) {
return { valid: false, error: 'Token expired' };
}
return { valid: true, payload };
} catch (error) {
return { valid: false, error: 'Token verification failed' };
}
}
// Refresh token (create new one with extended expiration)
export function refreshJWT(oldToken: string): { token?: string; error?: string } {
const verification = verifyJWT(oldToken);
if (!verification.valid || !verification.payload) {
return { error: verification.error || 'Invalid token' };
}
// Only allow refresh if token is not too old (within 7 days of expiration)
const now = Math.floor(Date.now() / 1000);
const daysUntilExpiry = (verification.payload.exp - now) / (24 * 60 * 60);
if (daysUntilExpiry > 7) {
return { error: 'Token not eligible for refresh yet' };
}
const newToken = createJWT(
verification.payload.sub,
verification.payload.email,
verification.payload.role
);
return { token: newToken };
}
// Extract token from Authorization header
export function extractTokenFromHeader(authHeader: string | null): string | null {
if (!authHeader) return null;
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return null;
}
return parts[1];
}
// Revoked tokens store (in production, use Redis or database)
const revokedTokens = new Set<string>();
// Revoke token
export function revokeJWT(token: string): void {
const verification = verifyJWT(token);
if (verification.valid && verification.payload?.jti) {
revokedTokens.add(verification.payload.jti);
}
}
// Check if token is revoked
export function isTokenRevoked(token: string): boolean {
const verification = verifyJWT(token);
if (!verification.valid || !verification.payload?.jti) {
return true;
}
return revokedTokens.has(verification.payload.jti);
}
// Clean up expired revoked tokens periodically
export function cleanupRevokedTokens(): void {
// This would query the database in production
// For now, we just clear the set periodically
if (revokedTokens.size > 10000) {
revokedTokens.clear();
}
}
// Decode JWT without verification (for debugging only)
export function decodeJWT(token: string): { header: any; payload: any } | null {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const header = JSON.parse(base64urlDecode(parts[0]));
const payload = JSON.parse(base64urlDecode(parts[1]));
return { header, payload };
} catch {
return null;
}
}