@theoptimalpartner/jwt-auth-validator
Version:
JWT token validation package with offline JWKS validation and Redis-based token revocation support
145 lines • 5.14 kB
JavaScript
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import NodeCache from 'node-cache';
import { hasClientSecret, safeCalculateSecretHash } from './cognito-utils.js';
export class JWKSService {
keyCache;
jwksClientInstance = 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.jwksClientInstance = jwksClient({
jwksUri: this.config.jwksUri,
cache: true,
cacheMaxAge: (this.config.cacheTimeout || 3600) * 1000,
rateLimit: true,
jwksRequestsPerMinute: 10,
});
}
}
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;
}
async getPublicKey(kid) {
return new Promise((resolve, reject) => {
const cachedKey = this.keyCache.get(kid);
if (cachedKey) {
return resolve(cachedKey);
}
if (!this.jwksClientInstance) {
return reject(new Error('JWKS client not initialized'));
}
this.jwksClientInstance.getSigningKey(kid, (err, key) => {
if (err) {
console.error('Error getting signing key:', err);
return reject(new Error(`Failed to get public key for kid: ${kid}`));
}
if (!key) {
return reject(new Error(`No key found for kid: ${kid}`));
}
try {
const publicKey = key.getPublicKey();
this.keyCache.set(kid, publicKey);
resolve(publicKey);
}
catch (error) {
const errorMessage = error instanceof Error
? error.message
: 'Unknown error';
reject(new Error(`Failed to get public key from signing key: ${errorMessage}`));
}
});
});
}
async validateTokenWithJWKS(token) {
try {
const decoded = jwt.decode(token, { complete: true });
if (!decoded?.header?.kid) {
throw new Error('Token header missing or invalid');
}
const header = decoded.header;
const publicKey = await this.getPublicKey(header.kid);
if (!this.config) {
throw new Error('JWKS configuration missing');
}
const verifyOptions = {
algorithms: ['RS256'],
issuer: this.config.issuer,
audience: this.config.audience,
clockTolerance: 60,
};
return jwt.verify(token, publicKey, verifyOptions);
}
catch (error) {
console.error('JWKS validation error:', error);
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() {
this.keyCache.flushAll();
}
getCacheStats() {
return {
cache: this.keyCache.getStats(),
config: this.config,
};
}
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.jwksClientInstance = null;
this.config = null;
this.keyCache.flushAll();
}
}
//# sourceMappingURL=jwks-service.js.map