Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix jwt algorithm #2104

Merged
merged 3 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 },
iamacook marked this conversation as resolved.
Show resolved Hide resolved
): 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