Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | 17x 17x 17x 17x 17x 18x 18x 18x 18x 18x 18x 18x 18x 18x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 17x 17x 17x 14x 14x 14x 14x 14x 14x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 106x 427x 427x 3x 1x 1x 2x 2x 2x 2x 2x 2x 1x 1x 1x | import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createPublicKey, generateKeyPairSync } from 'crypto';
import { exportJWK, JWK } from 'jose';
interface KeyPair {
kid: string;
privateKeyPem: string;
publicKeyPem: string;
alg: string; // e.g., RS256
}
@Injectable()
export class JwksService {
private readonly logger = new Logger(JwksService.name);
private readonly keys: Map<string, KeyPair> = new Map();
private currentKid: string | undefined;
private initialized = false;
constructor(private readonly config: ConfigService) {
this.initializeKeys();
}
private initializeKeys(): void {
try {
const algorithm = this.config.get<string>('JWT_ALGORITHM') || 'RS256';
// Preferred: JWKS_KEYS (base64 JSON array or {keys:[]})
const jwksBase64 = this.config.get<string>('JWKS_KEYS');
if (jwksBase64) {
try {
const decoded = Buffer.from(jwksBase64, 'base64').toString('utf8');
const parsed = JSON.parse(decoded);
const entries: any[] = Array.isArray(parsed) ? parsed : parsed?.keys || [];
for (const entry of entries) {
if (entry?.kid && entry?.privateKeyPem && entry?.publicKeyPem) {
const kp: KeyPair = {
kid: String(entry.kid),
alg: String(entry.alg || algorithm),
privateKeyPem: String(entry.privateKeyPem).replace(/\\n/g, '\n'),
publicKeyPem: String(entry.publicKeyPem).replace(/\\n/g, '\n'),
};
this.keys.set(kp.kid, kp);
if (!this.currentKid) this.currentKid = kp.kid;
}
}
this.initialized = this.keys.size > 0;
if (this.initialized) {
this.logger.log(`JWKS initialized from JWKS_KEYS with ${this.keys.size} key(s). Current KID: ${this.currentKid}`);
return;
}
} catch (e) {
this.logger.warn(`Failed to parse JWKS_KEYS: ${e instanceof Error ? e.message : String(e)}`);
}
}
// Fallback: use direct PEMs only when both provided
const privateKeyPem = this.config.get<string>('JWT_PRIVATE_KEY');
const publicKeyPem = this.config.get<string>('JWT_PUBLIC_KEY');
if (privateKeyPem && publicKeyPem) {
const keyPair: KeyPair = {
kid: 'main',
alg: algorithm,
privateKeyPem: privateKeyPem.replace(/\\n/g, '\n'),
publicKeyPem: publicKeyPem.replace(/\\n/g, '\n'),
};
this.keys.set(keyPair.kid, keyPair);
this.currentKid = keyPair.kid;
this.initialized = true;
this.logger.log(`JWKS initialized with fixed keypair. Algorithm: ${algorithm}, KID: ${this.currentKid}`);
return;
}
this.logger.warn('JWKS not initialized: supply JWKS_KEYS (preferred) or both JWT_PRIVATE_KEY and JWT_PUBLIC_KEY.');
// In test mode, generate a deterministic ephemeral keypair to enable auth flows
const isTest = process.env.NODE_ENV === 'test' || process.env.TEST_MODE === '1';
if (isTest) {
// Prefer keys from test global setup if present
const testPrivate = this.config.get<string>('JWT_PRIVATE_KEY');
const testPublic = this.config.get<string>('JWT_PUBLIC_KEY');
Iif (testPrivate && testPublic) {
const kp: KeyPair = {
kid: 'test-main',
alg: algorithm,
privateKeyPem: String(testPrivate).replace(/\\n/g, '\n'),
publicKeyPem: String(testPublic).replace(/\\n/g, '\n'),
};
this.keys.set(kp.kid, kp);
this.currentKid = kp.kid;
this.initialized = true;
this.logger.log(`JWKS initialized in test mode using provided test keys. KID: ${this.currentKid}`);
return;
}
// Otherwise, generate an ephemeral RSA keypair for tests
const { privateKey, publicKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});
const kp: KeyPair = {
kid: 'test-main',
alg: algorithm,
privateKeyPem: privateKey,
publicKeyPem: publicKey,
};
this.keys.set(kp.kid, kp);
this.currentKid = kp.kid;
this.initialized = true;
this.logger.log(`JWKS initialized in test mode using generated ephemeral keypair. KID: ${this.currentKid}`);
return;
}
this.initialized = false;
} catch (error) {
this.logger.error(`Failed to initialize JWKS service: ${error instanceof Error ? error.message : String(error)}`);
this.initialized = false;
}
}
/** Return whether the JWKS service has been properly initialized with keys */
get isInitialized(): boolean {
return this.initialized;
}
/** Return the active private key for signing */
get signingKey(): KeyPair | undefined {
Iif (!this.initialized || !this.currentKid) {
return undefined;
}
return this.keys.get(this.currentKid);
}
/** Return JWKS-compatible array of public keys */
async getPublicJwks(): Promise<{ keys: JWK[] }> {
if (!this.initialized) {
this.logger.warn('JWKS service not initialized - returning empty key set');
return { keys: [] };
}
const jwks: JWK[] = [];
for (const kp of this.keys.values()) {
try {
const keyLike = createPublicKey(kp.publicKeyPem);
const jwk = await exportJWK(keyLike) as JWK;
jwk.kid = kp.kid;
jwk.alg = kp.alg;
jwk.use = 'sig';
jwks.push(jwk);
} catch (error) {
// In certain environments the Node crypto decoder may not support the provided PEM; degrade gracefully
this.logger.warn(`Failed to export JWK for key ${kp.kid}: ${error instanceof Error ? error.message : String(error)}`);
}
}
return { keys: jwks };
}
async getPublicKeyPemByKid(kid: string): Promise<string | undefined> {
if (!this.initialized) {
this.logger.warn(`JWKS service not initialized - cannot get key for KID: ${kid}`);
return undefined;
}
return this.keys.get(kid)?.publicKeyPem;
}
} |