UNPKG

@theoptimalpartner/jwt-auth-validator

Version:

JWT token validation package with offline JWKS validation and Redis-based token revocation support

203 lines 7.7 kB
import { webcrypto } from "node:crypto"; if (!globalThis.crypto) { globalThis.crypto = webcrypto; } import * as jose from "jose"; import NodeCache from 'node-cache'; import { hasClientSecret, safeCalculateSecretHash } from './cognito-utils.js'; import { logError } from './error-utils.js'; export class JWKSService { keyCache; remoteJWKSet = null; config = null; constructor() { this.keyCache = new NodeCache({ stdTTL: 3600 }); } initialize(config) { this.config = { cacheTimeout: 3600, enableJWKSValidation: true, ...config, }; if (this.config.enableJWKSValidation && this.config.jwksUri) { this.remoteJWKSet = jose.createRemoteJWKSet(new URL(this.config.jwksUri)); if (process.env.NODE_ENV === 'development' || process.env.JWT_DEBUG === 'true') { console.log("JWKS Service initialized with remote JWKS set using jose library"); } } } static createCognitoConfig(region, userPoolId, clientId, clientSecret) { const config = { enableJWKSValidation: true, jwksUri: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`, issuer: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`, cacheTimeout: 3600, }; if (clientId) { config.audience = clientId; } if (clientSecret) { config.clientSecret = clientSecret; } return config; } getRemoteJWKSet() { if (!this.remoteJWKSet) { throw new Error("Remote JWKS set not initialized"); } return this.remoteJWKSet; } diagnoseTokenIssues(token) { const payload = jose.decodeJwt(token); const issues = []; if (!this.config) { issues.push("JWKS service not initialized - call validator.initialize() first"); } else { if (!this.config.audience) { issues.push("No audience configured in JWKS config"); } if (!this.config.issuer) { issues.push("No issuer configured in JWKS config"); } } if (!payload.aud && !payload.client_id) { issues.push("Token missing both 'aud' and 'client_id' claims"); } if (!payload.sub) { issues.push("Token missing 'sub' claim"); } if (payload.iss && !payload.iss.includes('cognito-idp')) { issues.push("Token issuer does not appear to be from Cognito"); } return { config: this.config ? { issuer: this.config.issuer, audience: this.config.audience, hasClientSecret: !!this.config.clientSecret } : null, payload: { aud: payload.aud, client_id: payload.client_id, sub: payload.sub, iss: payload.iss, token_use: payload.token_use, exp: payload.exp }, issues }; } async validateTokenWithJWKS(token) { try { if (process.env.NODE_ENV === 'development' || process.env.JWT_DEBUG === 'true') { console.log("Starting JWKS token validation with jose"); } if (!this.config) { throw new Error("JWKS configuration missing"); } const payload = jose.decodeJwt(token); if (process.env.NODE_ENV === 'development' || process.env.JWT_DEBUG === 'true') { console.log("JWKS Configuration:", { issuer: this.config.issuer, audience: this.config.audience, hasClientSecret: !!this.config.clientSecret }); console.log("Token payload aud/client_id:", { aud: payload.aud, client_id: payload.client_id, token_use: payload.token_use, iss: payload.iss }); } const JWKS = this.getRemoteJWKSet(); const verifyOptions = { issuer: this.config.issuer, clockTolerance: "60s", }; if (payload?.aud) { if (this.config.audience) { verifyOptions.audience = this.config.audience; } } else if (payload?.client_id) { if (payload.client_id !== this.config.audience) { throw new Error(`Token client_id (${payload.client_id}) does not match expected audience (${this.config.audience})`); } } else { console.error("Audience validation failed:", { tokenHasAud: !!payload?.aud, tokenHasClientId: !!payload?.client_id, configAudience: this.config.audience, configuredAudienceExists: !!this.config.audience }); throw new Error(`Token validation failed: Token missing both 'aud' and 'client_id' claims. ` + `Config audience: ${this.config.audience || 'undefined'}`); } if (process.env.NODE_ENV === 'development' || process.env.JWT_DEBUG === 'true') { console.log("Verify options:", verifyOptions); } const { payload: verifiedPayload } = await jose.jwtVerify(token, JWKS, verifyOptions); if (process.env.NODE_ENV === 'development' || process.env.JWT_DEBUG === 'true') { console.log("Token verified successfully with remote JWKS using jose"); } return verifiedPayload; } catch (error) { logError(error, 'JWKS token validation'); throw error; } } async validateCognitoToken(token) { try { const decoded = await this.validateTokenWithJWKS(token); if (decoded && typeof decoded === "object") { if (decoded.token_use && !["access", "id"].includes(decoded.token_use)) { throw new Error(`Invalid token_use: ${decoded.token_use}`); } if (!decoded.sub) { throw new Error("Token missing sub claim"); } if (decoded.iss && !decoded.iss.includes("cognito-idp")) { throw new Error("Invalid issuer for Cognito token"); } } return decoded; } catch (error) { throw error; } } invalidateCache() { if (this.remoteJWKSet) { console.log("JWKS remote set cache invalidated (handled internally by jose)"); } this.keyCache.flushAll(); } getCacheStats() { return { cache: this.keyCache.getStats(), config: this.config, remoteJWKSInitialized: !!this.remoteJWKSet, }; } getClientSecret() { return this.config?.clientSecret; } hasClientSecret() { return hasClientSecret(this.config?.clientSecret); } calculateSecretHash(identifier) { if (!this.config?.audience || !this.config?.clientSecret) { return ""; } return safeCalculateSecretHash(identifier, this.config.audience, this.config.clientSecret); } reset() { this.remoteJWKSet = null; this.config = null; this.keyCache.flushAll(); } } //# sourceMappingURL=jwks-service.js.map