// server/utils/jwt.ts /** * JWT Token Validation using jose library * * Validates Cidaas ID tokens (OIDC JWT) to ensure: * - Signature is valid (using Cidaas public keys from JWKS) * - Token has not expired * - Issuer matches expected Cidaas instance * - Audience matches our client ID */ import { jwtVerify, createRemoteJWKSet, type JWTPayload } from 'jose' // Cache JWKS (Cidaas public keys) to avoid fetching on every request let jwksCache: ReturnType | null = null /** * Get or create JWKS cache * * JWKS (JSON Web Key Set) contains public keys used to verify JWT signatures. * We cache this to improve performance. */ function getJWKS() { if (!jwksCache) { const config = useRuntimeConfig() jwksCache = createRemoteJWKSet(new URL(config.cidaas.jwksUrl)) } return jwksCache } /** * Extended JWT payload with OIDC claims */ export interface CidaasJWTPayload extends JWTPayload { sub: string // User ID (experimenta_id) email?: string email_verified?: boolean given_name?: string family_name?: string name?: string } /** * Verify Cidaas ID token * * @param idToken - JWT ID token from Cidaas * @returns Decoded and verified JWT payload * @throws Error if verification fails */ export async function verifyIdToken(idToken: string): Promise { const config = useRuntimeConfig() const JWKS = getJWKS() try { const { payload } = await jwtVerify(idToken, JWKS, { issuer: config.cidaas.issuer, // Must match Cidaas issuer audience: config.cidaas.clientId, // Must match our client ID }) return payload as CidaasJWTPayload } catch (error) { console.error('JWT verification failed:', error) // Provide specific error messages if (error instanceof Error) { if (error.message.includes('expired')) { throw createError({ statusCode: 401, statusMessage: 'Token has expired', }) } if (error.message.includes('signature')) { throw createError({ statusCode: 401, statusMessage: 'Invalid token signature', }) } } throw createError({ statusCode: 401, statusMessage: 'Invalid ID token', }) } } /** * Decode JWT without verification (for debugging only!) * * ⚠️ WARNING: Only use for debugging. Never trust unverified tokens! * * @param token - JWT token * @returns Decoded payload (unverified!) */ export function decodeJWT(token: string): JWTPayload | null { try { const parts = token.split('.') if (parts.length !== 3) { return null } const payload = parts[1] const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8')) return decoded } catch (error) { console.error('JWT decode failed:', error) return null } }