@theoptimalpartner/jwt-auth-validator
Version:
JWT token validation package with offline JWKS validation and Redis-based token revocation support
203 lines • 7.7 kB
JavaScript
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