From 6c0b81e5bd17cba77a2ade9087416e94118a88bf Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 6 Nov 2026 09:04:07 +1100 Subject: [PATCH 1/3] Require verified email through shared JWT verifier --- package.json | 2 +- pnpm-lock.yaml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 356f462..de1d348 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "qs": "^6.16.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "tc-core-library-js": "github:topcoder-platform/tc-core-library-js#v4.0.0-rc.2", + "tc-core-library-js": "https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4", "uuid": "^14.0.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7aad966..28439cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,8 +109,8 @@ importers: specifier: ^7.8.1 version: 7.8.2 tc-core-library-js: - specifier: github:topcoder-platform/tc-core-library-js#v4.0.0-rc.2 - version: https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/58de37670180e183e8378c3d203df0112a0414f6(supports-color@8.1.1) + specifier: https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4 + version: https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4(supports-color@8.1.1) uuid: specifier: 14.0.1 version: 14.0.1 @@ -3482,9 +3482,9 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - tc-core-library-js@https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/58de37670180e183e8378c3d203df0112a0414f6: - resolution: {gitHosted: true, tarball: https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/58de37670180e183e8378c3d203df0112a0414f6} - version: 4.0.0-rc.2 + tc-core-library-js@https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4: + resolution: {gitHosted: true, integrity: sha512-4xzx6ycEdy8Rs5BpIOUhg/YX0cUUQpOCtTM60vcb9JF4XvqI1fLfIDmHNvZNek49dBo1Il1Lp8O/HLhKfLhhVg==, tarball: https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4} + version: 4.0.0-rc.4 engines: {node: '>=22.13'} terser-webpack-plugin@5.6.1: @@ -7547,7 +7547,7 @@ snapshots: - bare-abort-controller - react-native-b4a - tc-core-library-js@https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/58de37670180e183e8378c3d203df0112a0414f6(supports-color@8.1.1): + tc-core-library-js@https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4(supports-color@8.1.1): dependencies: '@types/jsonwebtoken': 9.0.10 jsonwebtoken: 9.0.3 From 5147ea8374bd8b484089b1b1de770f542708673c Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 6 Nov 2026 09:22:01 +1100 Subject: [PATCH 2/3] Use compatible v4 verification backport and verified JWT fixtures --- package.json | 2 +- pnpm-lock.yaml | 12 +++++------ src/shared/modules/global/jwt.service.spec.ts | 20 +++++++++++-------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index de1d348..e6cf719 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "qs": "^6.16.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "tc-core-library-js": "https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4", + "tc-core-library-js": "https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/c2df92b2227089909ab6e229df0ec6b658bb431a", "uuid": "^14.0.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28439cd..dbe965a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,8 +109,8 @@ importers: specifier: ^7.8.1 version: 7.8.2 tc-core-library-js: - specifier: https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4 - version: https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4(supports-color@8.1.1) + specifier: https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/c2df92b2227089909ab6e229df0ec6b658bb431a + version: https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/c2df92b2227089909ab6e229df0ec6b658bb431a(supports-color@8.1.1) uuid: specifier: 14.0.1 version: 14.0.1 @@ -3482,9 +3482,9 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} - tc-core-library-js@https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4: - resolution: {gitHosted: true, integrity: sha512-4xzx6ycEdy8Rs5BpIOUhg/YX0cUUQpOCtTM60vcb9JF4XvqI1fLfIDmHNvZNek49dBo1Il1Lp8O/HLhKfLhhVg==, tarball: https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4} - version: 4.0.0-rc.4 + tc-core-library-js@https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/c2df92b2227089909ab6e229df0ec6b658bb431a: + resolution: {gitHosted: true, integrity: sha512-n+4KUOeV4LEIsTI/R216dBc6bUkbSyO/2O0LpoY3Hk7eqKsRQFUHloB3y0hfK/v1rzf8qYQxTdv8ikeRGBn4pQ==, tarball: https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/c2df92b2227089909ab6e229df0ec6b658bb431a} + version: 4.0.0-rc.2.1 engines: {node: '>=22.13'} terser-webpack-plugin@5.6.1: @@ -7547,7 +7547,7 @@ snapshots: - bare-abort-controller - react-native-b4a - tc-core-library-js@https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/2067e74f73d7fe3442c96f375fc97f8486f79be4(supports-color@8.1.1): + tc-core-library-js@https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/c2df92b2227089909ab6e229df0ec6b658bb431a(supports-color@8.1.1): dependencies: '@types/jsonwebtoken': 9.0.10 jsonwebtoken: 9.0.3 diff --git a/src/shared/modules/global/jwt.service.spec.ts b/src/shared/modules/global/jwt.service.spec.ts index 5b44ae0..800a740 100644 --- a/src/shared/modules/global/jwt.service.spec.ts +++ b/src/shared/modules/global/jwt.service.spec.ts @@ -66,6 +66,7 @@ describe('JwtService validation', () => { }); const token = sign( { + emailVerified: true, roles: [UserRole.Admin], userId: '12345', scope: 'openid profile offline_access', @@ -197,6 +198,7 @@ describe('JwtService validation', () => { }); const token = sign( { + emailVerified: true, scope: Scope.ReadGroups, gty: 'client-credentials', roles: [UserRole.Admin], @@ -249,7 +251,7 @@ describe('JwtService validation', () => { getPublicKey: () => publicKey.export({ type: 'spki', format: 'pem' }), }); const token = sign( - { roles: [UserRole.Admin], userId: '12345' }, + { emailVerified: true, roles: [UserRole.Admin], userId: '12345' }, privateKey, { algorithm: 'RS256', @@ -271,7 +273,7 @@ describe('JwtService validation', () => { it('rejects an unlisted issuer before requesting its signing key', async () => { const signingKeySpy = jest.spyOn(JwksClient.prototype, 'getSigningKey'); - const token = sign({ roles: [UserRole.Admin] }, privateKey, { + const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, privateKey, { algorithm: 'RS256', keyid: 'test-key-id', issuer: 'https://unlisted-issuer.example/', @@ -287,7 +289,7 @@ describe('JwtService validation', () => { it('rejects an RS256 token without a key ID', async () => { const signingKeySpy = jest.spyOn(JwksClient.prototype, 'getSigningKey'); - const token = sign({ roles: [UserRole.Admin] }, privateKey, { + const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, privateKey, { algorithm: 'RS256', issuer: ALLOWED_ISSUER, audience: AUDIENCE, @@ -303,6 +305,7 @@ describe('JwtService validation', () => { it('accepts a legacy HS256 admin token without kid or audience', async () => { const token = sign( { + emailVerified: true, roles: [UserRole.Admin], userId: '12345', handle: 'admin-user', @@ -326,7 +329,7 @@ describe('JwtService validation', () => { }); it('rejects an HS256 token signed with a different secret', async () => { - const token = sign({ roles: [UserRole.Admin] }, 'wrong-secret', { + const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, 'wrong-secret', { algorithm: 'HS256', issuer: ALLOWED_ISSUER, expiresIn: 300, @@ -339,7 +342,7 @@ describe('JwtService validation', () => { it('still verifies signatures when NODE_ENV is missing', async () => { delete process.env.NODE_ENV; - const token = sign({ roles: [UserRole.Admin] }, 'wrong-secret', { + const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, 'wrong-secret', { algorithm: 'HS256', issuer: ALLOWED_ISSUER, expiresIn: 300, @@ -352,7 +355,7 @@ describe('JwtService validation', () => { it('rejects an HS256 token when AUTH_SECRET is unavailable', async () => { AuthConfig.authSecret = undefined; - const token = sign({ roles: [UserRole.Admin] }, AUTH_SECRET, { + const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, AUTH_SECRET, { algorithm: 'HS256', issuer: ALLOWED_ISSUER, expiresIn: 300, @@ -364,7 +367,7 @@ describe('JwtService validation', () => { }); it('rejects an expired HS256 admin token', async () => { - const token = sign({ roles: [UserRole.Admin] }, AUTH_SECRET, { + const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, AUTH_SECRET, { algorithm: 'HS256', issuer: ALLOWED_ISSUER, expiresIn: -60, @@ -378,6 +381,7 @@ describe('JwtService validation', () => { it('accepts an HS256 admin token with a non-M2M audience', async () => { const token = sign( { + emailVerified: true, roles: [UserRole.Admin], userId: '12345', scope: 'openid profile offline_access', @@ -423,7 +427,7 @@ describe('JwtService validation', () => { }); it('rejects unsupported signing algorithms', async () => { - const token = sign({ roles: [UserRole.Admin] }, AUTH_SECRET, { + const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, AUTH_SECRET, { algorithm: 'HS384', issuer: ALLOWED_ISSUER, expiresIn: 300, From aa608c5e2fc15c4b2dd2523ccc860e676a862514 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 6 Nov 2026 09:24:14 +1100 Subject: [PATCH 3/3] Format verified JWT test fixtures --- src/shared/modules/global/jwt.service.spec.ts | 104 +++++++++++------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/src/shared/modules/global/jwt.service.spec.ts b/src/shared/modules/global/jwt.service.spec.ts index 800a740..46a959a 100644 --- a/src/shared/modules/global/jwt.service.spec.ts +++ b/src/shared/modules/global/jwt.service.spec.ts @@ -273,13 +273,17 @@ describe('JwtService validation', () => { it('rejects an unlisted issuer before requesting its signing key', async () => { const signingKeySpy = jest.spyOn(JwksClient.prototype, 'getSigningKey'); - const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, privateKey, { - algorithm: 'RS256', - keyid: 'test-key-id', - issuer: 'https://unlisted-issuer.example/', - audience: AUDIENCE, - expiresIn: 300, - }); + const token = sign( + { emailVerified: true, roles: [UserRole.Admin] }, + privateKey, + { + algorithm: 'RS256', + keyid: 'test-key-id', + issuer: 'https://unlisted-issuer.example/', + audience: AUDIENCE, + expiresIn: 300, + }, + ); await expect(service.validateToken(token)).rejects.toBeInstanceOf( UnauthorizedException, @@ -289,12 +293,16 @@ describe('JwtService validation', () => { it('rejects an RS256 token without a key ID', async () => { const signingKeySpy = jest.spyOn(JwksClient.prototype, 'getSigningKey'); - const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, privateKey, { - algorithm: 'RS256', - issuer: ALLOWED_ISSUER, - audience: AUDIENCE, - expiresIn: 300, - }); + const token = sign( + { emailVerified: true, roles: [UserRole.Admin] }, + privateKey, + { + algorithm: 'RS256', + issuer: ALLOWED_ISSUER, + audience: AUDIENCE, + expiresIn: 300, + }, + ); await expect(service.validateToken(token)).rejects.toBeInstanceOf( UnauthorizedException, @@ -329,11 +337,15 @@ describe('JwtService validation', () => { }); it('rejects an HS256 token signed with a different secret', async () => { - const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, 'wrong-secret', { - algorithm: 'HS256', - issuer: ALLOWED_ISSUER, - expiresIn: 300, - }); + const token = sign( + { emailVerified: true, roles: [UserRole.Admin] }, + 'wrong-secret', + { + algorithm: 'HS256', + issuer: ALLOWED_ISSUER, + expiresIn: 300, + }, + ); await expect(service.validateToken(token)).rejects.toBeInstanceOf( UnauthorizedException, @@ -342,11 +354,15 @@ describe('JwtService validation', () => { it('still verifies signatures when NODE_ENV is missing', async () => { delete process.env.NODE_ENV; - const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, 'wrong-secret', { - algorithm: 'HS256', - issuer: ALLOWED_ISSUER, - expiresIn: 300, - }); + const token = sign( + { emailVerified: true, roles: [UserRole.Admin] }, + 'wrong-secret', + { + algorithm: 'HS256', + issuer: ALLOWED_ISSUER, + expiresIn: 300, + }, + ); await expect(service.validateToken(token)).rejects.toBeInstanceOf( UnauthorizedException, @@ -355,11 +371,15 @@ describe('JwtService validation', () => { it('rejects an HS256 token when AUTH_SECRET is unavailable', async () => { AuthConfig.authSecret = undefined; - const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, AUTH_SECRET, { - algorithm: 'HS256', - issuer: ALLOWED_ISSUER, - expiresIn: 300, - }); + const token = sign( + { emailVerified: true, roles: [UserRole.Admin] }, + AUTH_SECRET, + { + algorithm: 'HS256', + issuer: ALLOWED_ISSUER, + expiresIn: 300, + }, + ); await expect(service.validateToken(token)).rejects.toBeInstanceOf( UnauthorizedException, @@ -367,11 +387,15 @@ describe('JwtService validation', () => { }); it('rejects an expired HS256 admin token', async () => { - const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, AUTH_SECRET, { - algorithm: 'HS256', - issuer: ALLOWED_ISSUER, - expiresIn: -60, - }); + const token = sign( + { emailVerified: true, roles: [UserRole.Admin] }, + AUTH_SECRET, + { + algorithm: 'HS256', + issuer: ALLOWED_ISSUER, + expiresIn: -60, + }, + ); await expect(service.validateToken(token)).rejects.toBeInstanceOf( UnauthorizedException, @@ -427,11 +451,15 @@ describe('JwtService validation', () => { }); it('rejects unsupported signing algorithms', async () => { - const token = sign({ emailVerified: true, roles: [UserRole.Admin] }, AUTH_SECRET, { - algorithm: 'HS384', - issuer: ALLOWED_ISSUER, - expiresIn: 300, - }); + const token = sign( + { emailVerified: true, roles: [UserRole.Admin] }, + AUTH_SECRET, + { + algorithm: 'HS384', + issuer: ALLOWED_ISSUER, + expiresIn: 300, + }, + ); await expect(service.validateToken(token)).rejects.toBeInstanceOf( UnauthorizedException,