Skip to content

Commit

Permalink
Fix jwt algorithm (#2104)
Browse files Browse the repository at this point in the history
* Fixes an issue where the JWT algorithm was incorrectly passed in the payload when signing and decoding
  • Loading branch information
PooyaRaki authored Nov 8, 2024
1 parent d51ef03 commit f666253
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 7 deletions.
17 changes: 14 additions & 3 deletions src/datasources/jwt/jwt.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,24 +30,35 @@ function jwtClientFactory() {
...rest,
},
options.secretOrPrivateKey,
{ algorithm: options.algorithm },
);
},
verify: <T extends object>(
token: string,
options: { issuer: string; secretOrPrivateKey: string },
options: {
issuer: string;
secretOrPrivateKey: string;
algorithms?: Array<jwt.Algorithm>;
},
): 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,
}) as T;
},
decode: <T extends object>(
token: string,
options: { issuer: string; secretOrPrivateKey: string },
options: {
issuer: string;
secretOrPrivateKey: string;
algorithms?: Array<jwt.Algorithm>;
},
): JwtPayloadWithClaims<T> => {
// 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,
Expand Down
2 changes: 2 additions & 0 deletions src/datasources/jwt/jwt.service.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { JwtPayloadWithClaims } from '@/datasources/jwt/jwt-claims.entity';
import type { Algorithm } from 'jsonwebtoken';

export const IJwtService = Symbol('IJwtService');

Expand All @@ -13,6 +14,7 @@ export interface IJwtService {
payload: T,
options?: {
secretOrPrivateKey: string;
algorithm?: Algorithm;
},
): string;

Expand Down
58 changes: 58 additions & 0 deletions src/datasources/jwt/jwt.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe('JwtService', () => {
{ iss: configIssuer, ...payload },
{
secretOrPrivateKey: configSecret,
algorithm: 'HS256',
},
);
});
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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'],
});
});
});
});
19 changes: 15 additions & 4 deletions src/datasources/jwt/jwt.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -27,7 +30,7 @@ export class JwtService implements IJwtService {
},
>(
payload: T,
options: { secretOrPrivateKey: string } = {
options: { secretOrPrivateKey: string; algorithm?: Algorithm } = {
secretOrPrivateKey: this.secret,
},
): string {
Expand All @@ -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<T extends object>(
token: string,
options: { issuer: string; secretOrPrivateKey: string } = {
options: {
issuer: string;
secretOrPrivateKey: string;
algorithms?: Array<Algorithm>;
} = {
issuer: this.issuer,
secretOrPrivateKey: this.secret,
},
Expand All @@ -52,7 +59,11 @@ export class JwtService implements IJwtService {

decode<T extends object>(
token: string,
options: { issuer: string; secretOrPrivateKey: string } = {
options: {
issuer: string;
secretOrPrivateKey: string;
algorithms?: Array<Algorithm>;
} = {
issuer: this.issuer,
secretOrPrivateKey: this.secret,
},
Expand Down

0 comments on commit f666253

Please sign in to comment.