diff --git a/src/datasources/jwt/jwt.module.ts b/src/datasources/jwt/jwt.module.ts index 25e97fe39e..6fd0f1d7be 100644 --- a/src/datasources/jwt/jwt.module.ts +++ b/src/datasources/jwt/jwt.module.ts @@ -17,7 +17,7 @@ function jwtClientFactory() { }, >( payload: T, - options: { secretOrPrivateKey: string }, + options: { secretOrPrivateKey: string; algorithm?: jwt.Algorithm }, ): string => { // All date-based claims should be second-based NumericDates const { exp, iat, nbf, ...rest } = payload; @@ -30,13 +30,19 @@ function jwtClientFactory() { ...rest, }, options.secretOrPrivateKey, + { algorithm: options.algorithm }, ); }, verify: ( token: string, - options: { issuer: string; secretOrPrivateKey: string }, + options: { + issuer: string; + secretOrPrivateKey: string; + algorithms?: Array; + }, ): T => { return jwt.verify(token, options.secretOrPrivateKey, { + algorithms: options.algorithms, issuer: options.issuer, // Return only payload without claims, e.g. no exp, nbf, etc. complete: false, @@ -44,10 +50,15 @@ function jwtClientFactory() { }, decode: ( token: string, - options: { issuer: string; secretOrPrivateKey: string }, + options: { + issuer: string; + secretOrPrivateKey: string; + algorithms?: Array; + }, ): JwtPayloadWithClaims => { // Client has `decode` method but we also want to verify the signature const { payload } = jwt.verify(token, options.secretOrPrivateKey, { + algorithms: options.algorithms, issuer: options.issuer, // Return headers, payload (with claims) and signature complete: true, diff --git a/src/datasources/jwt/jwt.service.interface.ts b/src/datasources/jwt/jwt.service.interface.ts index a84eafbc35..a1121300cb 100644 --- a/src/datasources/jwt/jwt.service.interface.ts +++ b/src/datasources/jwt/jwt.service.interface.ts @@ -1,4 +1,5 @@ import type { JwtPayloadWithClaims } from '@/datasources/jwt/jwt-claims.entity'; +import type { Algorithm } from 'jsonwebtoken'; export const IJwtService = Symbol('IJwtService'); @@ -13,6 +14,7 @@ export interface IJwtService { payload: T, options?: { secretOrPrivateKey: string; + algorithm?: Algorithm; }, ): string; diff --git a/src/datasources/jwt/jwt.service.spec.ts b/src/datasources/jwt/jwt.service.spec.ts index 8783465231..1ae3b3a685 100644 --- a/src/datasources/jwt/jwt.service.spec.ts +++ b/src/datasources/jwt/jwt.service.spec.ts @@ -44,6 +44,7 @@ describe('JwtService', () => { { iss: configIssuer, ...payload }, { secretOrPrivateKey: configSecret, + algorithm: 'HS256', }, ); }); @@ -63,8 +64,27 @@ describe('JwtService', () => { expect(jwtClientMock.sign).toHaveBeenCalledTimes(1); expect(jwtClientMock.sign).toHaveBeenCalledWith(payload, { secretOrPrivateKey: customSecret, + algorithm: 'HS256', }); }); + + it('should sign a payload with RS256 algorithm', () => { + const payload = JSON.parse(fakeJson()) as object; + + service.sign(payload, { + secretOrPrivateKey: configSecret, + algorithm: 'RS256', + }); + + expect(jwtClientMock.sign).toHaveBeenCalledTimes(1); + expect(jwtClientMock.sign).toHaveBeenCalledWith( + { iss: configIssuer, ...payload }, + { + secretOrPrivateKey: configSecret, + algorithm: 'RS256', + }, + ); + }); }); describe('verify', () => { @@ -96,6 +116,25 @@ describe('JwtService', () => { secretOrPrivateKey: customSecret, }); }); + + it('should verify a token with RS256 algorithm', () => { + const token = faker.string.alphanumeric(); + const customIssuer = faker.word.noun(); + const customSecret = faker.string.alphanumeric(); + + service.verify(token, { + issuer: customIssuer, + secretOrPrivateKey: customSecret, + algorithms: ['RS256'], + }); + + expect(jwtClientMock.verify).toHaveBeenCalledTimes(1); + expect(jwtClientMock.verify).toHaveBeenCalledWith(token, { + issuer: customIssuer, + secretOrPrivateKey: customSecret, + algorithms: ['RS256'], + }); + }); }); describe('decode', () => { @@ -127,5 +166,24 @@ describe('JwtService', () => { secretOrPrivateKey: customSecret, }); }); + + it('should decode a token with RS256 Algorithm', () => { + const token = faker.string.alphanumeric(); + const customIssuer = faker.word.noun(); + const customSecret = faker.string.alphanumeric(); + + service.decode(token, { + issuer: customIssuer, + secretOrPrivateKey: customSecret, + algorithms: ['RS256'], + }); + + expect(jwtClientMock.decode).toHaveBeenCalledTimes(1); + expect(jwtClientMock.decode).toHaveBeenCalledWith(token, { + issuer: customIssuer, + secretOrPrivateKey: customSecret, + algorithms: ['RS256'], + }); + }); }); }); diff --git a/src/datasources/jwt/jwt.service.ts b/src/datasources/jwt/jwt.service.ts index 53c3cc0821..82b8e0a24c 100644 --- a/src/datasources/jwt/jwt.service.ts +++ b/src/datasources/jwt/jwt.service.ts @@ -3,9 +3,12 @@ import { IJwtService } from '@/datasources/jwt/jwt.service.interface'; import { JwtPayloadWithClaims } from '@/datasources/jwt/jwt-claims.entity'; import { Inject, Injectable } from '@nestjs/common'; import { IConfigurationService } from '@/config/configuration.service.interface'; +import type { Algorithm } from 'jsonwebtoken'; @Injectable() export class JwtService implements IJwtService { + private static readonly ALGORITHM: Algorithm = 'HS256'; + issuer: string; secret: string; @@ -27,7 +30,7 @@ export class JwtService implements IJwtService { }, >( payload: T, - options: { secretOrPrivateKey: string } = { + options: { secretOrPrivateKey: string; algorithm?: Algorithm } = { secretOrPrivateKey: this.secret, }, ): string { @@ -36,13 +39,17 @@ export class JwtService implements IJwtService { iss: 'iss' in payload ? payload.iss : this.issuer, ...payload, }, - options, + { ...options, algorithm: options.algorithm ?? JwtService.ALGORITHM }, ); } verify( token: string, - options: { issuer: string; secretOrPrivateKey: string } = { + options: { + issuer: string; + secretOrPrivateKey: string; + algorithms?: Array; + } = { issuer: this.issuer, secretOrPrivateKey: this.secret, }, @@ -52,7 +59,11 @@ export class JwtService implements IJwtService { decode( token: string, - options: { issuer: string; secretOrPrivateKey: string } = { + options: { + issuer: string; + secretOrPrivateKey: string; + algorithms?: Array; + } = { issuer: this.issuer, secretOrPrivateKey: this.secret, },