diff --git a/package.json b/package.json index 73cbbce..047c87b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "csv-stringify": "^6.5.2", "dotenv": "^16.5.0", "jsonwebtoken": "^9.0.2", + "jwks-rsa": "^3.2.0", "lodash": "^4.17.21", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10d0213..5fd9f1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 + jwks-rsa: + specifier: ^3.2.0 + version: 3.2.0 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -1200,9 +1203,15 @@ packages: '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + '@types/express-serve-static-core@5.0.6': resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} + '@types/express@4.17.21': + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + '@types/express@5.0.1': resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==} @@ -1230,6 +1239,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.9': + resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==} + '@types/lodash@4.17.16': resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==} @@ -1239,6 +1251,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.14.1': resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==} @@ -2643,6 +2658,9 @@ packages: node-notifier: optional: true + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2692,6 +2710,10 @@ packages: jwa@1.4.1: resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + jwks-rsa@3.2.0: + resolution: {integrity: sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==} + engines: {node: '>=14'} + jws@3.2.2: resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} @@ -2720,6 +2742,9 @@ packages: libphonenumber-js@1.12.7: resolution: {integrity: sha512-0nYZSNj/QEikyhcM5RZFXGlCB/mr4PVamnT1C2sKBnDDTYndrvbybYjvg+PMqAndQHlLbwQ3socolnL3WWTUFA==} + limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2739,6 +2764,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -2788,6 +2816,13 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-memoizer@2.3.0: + resolution: {integrity: sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -3753,6 +3788,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -4890,6 +4928,13 @@ snapshots: '@types/estree@1.0.7': {} + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 22.14.1 + '@types/qs': 6.9.18 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + '@types/express-serve-static-core@5.0.6': dependencies: '@types/node': 22.14.1 @@ -4897,6 +4942,13 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 0.17.4 + '@types/express@4.17.21': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.9.18 + '@types/serve-static': 1.15.7 + '@types/express@5.0.1': dependencies: '@types/body-parser': 1.19.5 @@ -4928,12 +4980,19 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.9': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.14.1 + '@types/lodash@4.17.16': {} '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} + '@types/ms@2.1.0': {} + '@types/node@22.14.1': dependencies: undici-types: 6.21.0 @@ -6677,6 +6736,8 @@ snapshots: - supports-color - ts-node + jose@4.15.9: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -6729,6 +6790,17 @@ snapshots: ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 + jwks-rsa@3.2.0: + dependencies: + '@types/express': 4.17.21 + '@types/jsonwebtoken': 9.0.9 + debug: 4.4.0 + jose: 4.15.9 + limiter: 1.1.5 + lru-memoizer: 2.3.0 + transitivePeerDependencies: + - supports-color + jws@3.2.2: dependencies: jwa: 1.4.1 @@ -6753,6 +6825,8 @@ snapshots: libphonenumber-js@1.12.7: {} + limiter@1.1.5: {} + lines-and-columns@1.2.4: {} load-esm@1.0.2: {} @@ -6767,6 +6841,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.clonedeep@4.5.0: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} @@ -6809,6 +6885,15 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-memoizer@2.3.0: + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 6.0.0 + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -7762,6 +7847,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/src/config/config.env.ts b/src/config/config.env.ts index 222c4ed..f8c0194 100644 --- a/src/config/config.env.ts +++ b/src/config/config.env.ts @@ -31,9 +31,6 @@ export class ConfigEnv { @IsString() AUTH0_M2M_GRANT_TYPE!: string; - @IsString() - AUTH0_CERT!: string; - @IsString() AUTH0_CLIENT_ID!: string; diff --git a/src/core/auth/jwt.ts b/src/core/auth/jwt.ts new file mode 100644 index 0000000..a673697 --- /dev/null +++ b/src/core/auth/jwt.ts @@ -0,0 +1,52 @@ +import { Logger } from '@nestjs/common'; +import { decode } from 'jsonwebtoken'; +import { JwksClient } from 'jwks-rsa'; +import { ENV_CONFIG } from 'src/config'; + +const logger = new Logger(`auth/jwks`); + +const client = new JwksClient({ + jwksUri: `${ENV_CONFIG.AUTH0_M2M_TOKEN_URL}/.well-known/jwks.json`, + cache: true, + rateLimit: true, +}); + +/** + * Retrieves the signing key for a given JWT token. + * + * This function decodes the token to extract its header and uses the `kid` (Key ID) + * to fetch the corresponding signing key from a remote client. The signing key is + * then resolved as a public key. + * + * @param token - The JWT token for which the signing key is to be retrieved. + * @returns A promise that resolves with the public signing key as a string. + * @throws An error if the token is invalid, the `kid` is missing, or the signing key + * cannot be retrieved or resolved. + */ +export const getSigningKey = (token: string) => { + const tokenHeader = decode(token, { complete: true })?.header; + + return new Promise((resolve, reject) => { + if (!tokenHeader || !tokenHeader.kid) { + logger.error('Invalid token: Missing key ID'); + return reject(new Error('Invalid token: Missing key ID')); + } + + client.getSigningKey(tokenHeader.kid, function (err, key) { + if (err || !key) { + logger.error('Error getting signing key:', err); + return reject(new Error('Invalid token: Unable to get signing key')); + } + + // Get the public key using the proper method + const signingKey = key.getPublicKey(); + + if (!signingKey) { + logger.error('Error getting public key!'); + return reject(new Error('Invalid token: Unable to get public key')); + } + + resolve(signingKey); + }); + }); +}; diff --git a/src/core/auth/middleware/tokenValidator.middleware.ts b/src/core/auth/middleware/tokenValidator.middleware.ts index 680a36c..ae75df0 100644 --- a/src/core/auth/middleware/tokenValidator.middleware.ts +++ b/src/core/auth/middleware/tokenValidator.middleware.ts @@ -6,12 +6,13 @@ import { } from '@nestjs/common'; import * as jwt from 'jsonwebtoken'; import { ENV_CONFIG } from 'src/config'; +import { getSigningKey } from '../jwt'; const logger = new Logger(`Auth/TokenValidatorMiddleware`); @Injectable() export class TokenValidatorMiddleware implements NestMiddleware { - use(req: any, res: Response, next: (error?: any) => void) { + async use(req: any, res: Response, next: (error?: any) => void) { const [type, idToken] = req.headers.authorization?.split(' ') ?? []; if (type !== 'Bearer' || !idToken) { @@ -20,7 +21,8 @@ export class TokenValidatorMiddleware implements NestMiddleware { let decoded: any; try { - decoded = jwt.verify(idToken, ENV_CONFIG.AUTH0_CERT); + const signingKey = await getSigningKey(idToken); + decoded = jwt.verify(idToken, signingKey); } catch (error) { logger.error('Error verifying JWT', error); throw new UnauthorizedException('Invalid or expired JWT!');