Skip to content

Commit 226c60c

Browse files
committed
test(fxa-auth-server): migrate 16 OAuth Mocha test files to Jest (FXA-12565)
1 parent 98a9c12 commit 226c60c

24 files changed

+7963
-38
lines changed

packages/fxa-auth-server/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module.exports = {
2020
'^fxa-shared/(.*)$': '<rootDir>/../fxa-shared/$1',
2121
},
2222
testTimeout: 10000,
23+
maxWorkers: 4,
2324
clearMocks: true,
2425
workerIdleMemoryLimit: '512MB',
2526
setupFiles: ['<rootDir>/jest.setup.js', '<rootDir>/jest.setup-proxyquire.js'],

packages/fxa-auth-server/jest.integration.config.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ module.exports = {
1212
moduleNameMapper: {
1313
...baseConfig.moduleNameMapper,
1414
'^@fxa/vendored/(.*)$': '<rootDir>/../../libs/vendored/$1/src',
15+
// Map bare 'fxa-shared' to source to avoid class identity mismatches
16+
// between dist/cjs and source modules (e.g., ScopeSet loaded via
17+
// require('fxa-shared').oauth.scopes vs fxa-shared/oauth/scopes).
18+
'^fxa-shared$': '<rootDir>/../fxa-shared/index',
1519
},
1620

1721
testMatch: [
@@ -25,13 +29,8 @@ module.exports = {
2529
globalSetup: '<rootDir>/test/support/jest-global-setup.ts',
2630
globalTeardown: '<rootDir>/test/support/jest-global-teardown.ts',
2731

28-
setupFiles: [
29-
'<rootDir>/test/support/jest-setup-env.ts',
30-
],
31-
32-
setupFilesAfterEnv: [
33-
'<rootDir>/test/support/jest-setup-integration.ts',
34-
],
32+
setupFiles: ['<rootDir>/test/support/jest-setup-env.ts'],
33+
setupFilesAfterEnv: ['<rootDir>/test/support/jest-setup-integration.ts'],
3534

3635
collectCoverageFrom: [
3736
'lib/**/*.{ts,js}',

packages/fxa-auth-server/jest.setup.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
/**
66
* Jest global setup - runs before each test file.
77
*
8-
* The OAuth keys exist in config/key.json but the config module's path
9-
* resolution can behave differently under Jest's module transformation.
10-
* This sets the env var to allow tests to run without requiring the
11-
* OAuth key validation (which is a runtime concern, not a unit test concern).
8+
* Set NODE_ENV=dev so that the config module loads config/dev.json,
9+
* which includes OAuth keys (config/key.json), authServerSecrets,
10+
* and other values required by unit tests. This matches the CI test
11+
* runner (scripts/test-ci.sh) which also exports NODE_ENV=dev.
12+
*
13+
* Jest defaults NODE_ENV to 'test', but there is no config/test.json
14+
* in this package, so tests fail without the dev config overlay.
1215
*/
13-
14-
process.env.FXA_OPENID_UNSAFELY_ALLOW_MISSING_ACTIVE_KEY = 'true';
16+
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'test') {
17+
process.env.NODE_ENV = 'dev';
18+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
export {};
2+
3+
/* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6+
7+
const cloneDeep = require('lodash/cloneDeep');
8+
const util = require('util');
9+
10+
const jwt = require('jsonwebtoken');
11+
const jwtSign = util.promisify(jwt.sign);
12+
13+
const { config } = require('../../config');
14+
const unique = require('./unique');
15+
16+
const verifyAssertion = require('./assertion');
17+
18+
const ISSUER = config.get('oauthServer.browserid.issuer');
19+
const AUDIENCE = config.get('publicUrl');
20+
const USERID = unique(16).toString('hex');
21+
const GENERATION = 12345;
22+
const VERIFIED_EMAIL = 'test@exmample.com';
23+
const LAST_AUTH_AT = Date.now();
24+
const AMR = ['pwd', 'otp'];
25+
const AAL = 2;
26+
const PROFILE_CHANGED_AT = Date.now();
27+
const JWT_IAT = Date.now();
28+
29+
const ERRNO_INVALID_ASSERTION = 104;
30+
31+
const GOOD_CLAIMS = {
32+
'fxa-generation': GENERATION,
33+
'fxa-verifiedEmail': VERIFIED_EMAIL,
34+
'fxa-lastAuthAt': LAST_AUTH_AT,
35+
'fxa-tokenVerified': true,
36+
'fxa-amr': AMR,
37+
'fxa-aal': AAL,
38+
'fxa-profileChangedAt': PROFILE_CHANGED_AT,
39+
};
40+
41+
const AUTH_SERVER_SECRETS = config.get('oauthServer.authServerSecrets');
42+
43+
async function makeJWT(
44+
claims: Record<string, any>,
45+
key: string = AUTH_SERVER_SECRETS[0],
46+
options: Record<string, any> = {}
47+
) {
48+
const mergedClaims = {
49+
iat: JWT_IAT,
50+
exp: JWT_IAT + 60,
51+
sub: USERID,
52+
aud: AUDIENCE,
53+
iss: ISSUER,
54+
...claims,
55+
};
56+
const mergedOptions = {
57+
algorithm: 'HS256',
58+
...options,
59+
};
60+
return await jwtSign(mergedClaims, key, mergedOptions);
61+
}
62+
63+
describe('JWT verifyAssertion', () => {
64+
it('should accept well-formed JWT assertions', async () => {
65+
expect(AUTH_SERVER_SECRETS.length).toBeGreaterThanOrEqual(1);
66+
67+
const assertion = await makeJWT(GOOD_CLAIMS);
68+
const claims = await verifyAssertion(assertion);
69+
expect(claims).toEqual({
70+
iat: JWT_IAT,
71+
uid: USERID,
72+
'fxa-generation': GENERATION,
73+
'fxa-verifiedEmail': VERIFIED_EMAIL,
74+
'fxa-lastAuthAt': LAST_AUTH_AT,
75+
'fxa-tokenVerified': true,
76+
'fxa-amr': AMR,
77+
'fxa-aal': AAL,
78+
'fxa-profileChangedAt': PROFILE_CHANGED_AT,
79+
});
80+
});
81+
82+
it('should accept JWTs signed with an alternate key', async () => {
83+
expect(AUTH_SERVER_SECRETS.length).toBeGreaterThanOrEqual(2);
84+
const assertion = await makeJWT(GOOD_CLAIMS, AUTH_SERVER_SECRETS[1]);
85+
const claims = await verifyAssertion(assertion);
86+
expect(claims).toEqual({
87+
iat: JWT_IAT,
88+
uid: USERID,
89+
'fxa-generation': GENERATION,
90+
'fxa-verifiedEmail': VERIFIED_EMAIL,
91+
'fxa-lastAuthAt': LAST_AUTH_AT,
92+
'fxa-tokenVerified': true,
93+
'fxa-amr': AMR,
94+
'fxa-aal': AAL,
95+
'fxa-profileChangedAt': PROFILE_CHANGED_AT,
96+
});
97+
});
98+
99+
it('should reject JWTs signed with an unknown key', async () => {
100+
const assertion = await makeJWT(GOOD_CLAIMS, 'whereDidThisComeFrom?');
101+
await expect(verifyAssertion(assertion)).rejects.toHaveProperty(
102+
'errno',
103+
ERRNO_INVALID_ASSERTION
104+
);
105+
});
106+
107+
it('should reject expired JWTs', async () => {
108+
const assertion = await makeJWT({
109+
...GOOD_CLAIMS,
110+
exp: Math.floor(Date.now() / 1000) - 60,
111+
});
112+
await expect(verifyAssertion(assertion)).rejects.toHaveProperty(
113+
'errno',
114+
ERRNO_INVALID_ASSERTION
115+
);
116+
});
117+
118+
it('should reject JWTs with incorrect audience', async () => {
119+
const assertion = await makeJWT({
120+
...GOOD_CLAIMS,
121+
aud: 'https://example.com',
122+
});
123+
await expect(verifyAssertion(assertion)).rejects.toHaveProperty(
124+
'errno',
125+
ERRNO_INVALID_ASSERTION
126+
);
127+
});
128+
129+
it('should reject JWTs with unexpected algorithms', async () => {
130+
const assertion = await makeJWT(GOOD_CLAIMS, AUTH_SERVER_SECRETS[0], {
131+
algorithm: 'HS384',
132+
});
133+
await expect(verifyAssertion(assertion)).rejects.toHaveProperty(
134+
'errno',
135+
ERRNO_INVALID_ASSERTION
136+
);
137+
});
138+
139+
it('should reject JWTs from non-allowed issuers', async () => {
140+
const assertion = await makeJWT({
141+
...GOOD_CLAIMS,
142+
iss: 'evil.com',
143+
});
144+
await expect(verifyAssertion(assertion)).rejects.toHaveProperty(
145+
'errno',
146+
ERRNO_INVALID_ASSERTION
147+
);
148+
});
149+
150+
it('should reject JWTs with malformed user id', async () => {
151+
const assertion = await makeJWT({
152+
...GOOD_CLAIMS,
153+
sub: 'non-hex-string',
154+
});
155+
await expect(verifyAssertion(assertion)).rejects.toHaveProperty(
156+
'errno',
157+
ERRNO_INVALID_ASSERTION
158+
);
159+
});
160+
161+
it('should reject JWTs with missing `lastAuthAt` claim', async () => {
162+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
163+
const { 'fxa-lastAuthAt': _removed, ...claimsWithoutLastAuth } = GOOD_CLAIMS;
164+
const assertion = await makeJWT(claimsWithoutLastAuth);
165+
await expect(verifyAssertion(assertion)).rejects.toHaveProperty(
166+
'errno',
167+
ERRNO_INVALID_ASSERTION
168+
);
169+
});
170+
171+
it('should accept JWTs with missing `amr` claim', async () => {
172+
let claims: any = cloneDeep(GOOD_CLAIMS);
173+
delete claims['fxa-amr'];
174+
const assertion = await makeJWT(claims);
175+
claims = await verifyAssertion(assertion);
176+
expect(Object.keys(claims).sort()).toEqual([
177+
'fxa-aal',
178+
'fxa-generation',
179+
'fxa-lastAuthAt',
180+
'fxa-profileChangedAt',
181+
'fxa-tokenVerified',
182+
'fxa-verifiedEmail',
183+
'iat',
184+
'uid',
185+
]);
186+
});
187+
188+
it('should accept assertions with missing `aal` claim', async () => {
189+
let claims: any = cloneDeep(GOOD_CLAIMS);
190+
delete claims['fxa-aal'];
191+
const assertion = await makeJWT(claims);
192+
claims = await verifyAssertion(assertion);
193+
expect(Object.keys(claims).sort()).toEqual([
194+
'fxa-amr',
195+
'fxa-generation',
196+
'fxa-lastAuthAt',
197+
'fxa-profileChangedAt',
198+
'fxa-tokenVerified',
199+
'fxa-verifiedEmail',
200+
'iat',
201+
'uid',
202+
]);
203+
});
204+
});

0 commit comments

Comments
 (0)