Skip to content

Commit cabad89

Browse files
authored
Merge pull request #19468 from mozilla/feat/mfa-guard-utils
chore(mfa): Add mfa helper utility for checking invalid JWT
2 parents e809b98 + b6d8321 commit cabad89

2 files changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import { JwtTokenCache, MfaOtpRequestCache } from './cache';
6+
import {
7+
clearMfaAndJwtCacheOnInvalidJwt,
8+
isInvalidJwtError,
9+
} from './mfa-guard-utils';
10+
11+
const defaultSessionToken = 'you-get-a-session-token';
12+
const jwt = 'and-you-get-a-jwt';
13+
const scope = 'test';
14+
15+
jest.mock('./cache', () => {
16+
const actual = jest.requireActual('./cache');
17+
return {
18+
__esModule: true,
19+
...actual,
20+
sessionToken: jest.fn(() => defaultSessionToken),
21+
};
22+
});
23+
24+
describe('mfa-guard-utils', () => {
25+
let sessionTokenSpy: jest.SpyInstance;
26+
27+
beforeEach(() => {
28+
sessionTokenSpy = jest.mocked(require('./cache').sessionToken);
29+
});
30+
31+
afterEach(() => {
32+
sessionTokenSpy.mockReturnValue(defaultSessionToken);
33+
});
34+
35+
describe('isInvalidJwtError', () => {
36+
it('should return true if the error is an invalid JWT error', () => {
37+
expect(isInvalidJwtError({ code: 401, errno: 110 })).toBe(true);
38+
});
39+
40+
it('should return false if the error is not an invalid JWT error', () => {
41+
expect(isInvalidJwtError({ code: 401, errno: 100 })).toBe(false);
42+
});
43+
44+
it('should return false if the error is not an object', () => {
45+
expect(isInvalidJwtError('not-an-object')).toBe(false);
46+
});
47+
48+
it('should return false if the error is not an object with code and errno properties', () => {
49+
expect(isInvalidJwtError({ code: 401 })).toBe(false);
50+
});
51+
});
52+
53+
describe('clearMfaAndJwtCacheOnInvalidJwt', () => {
54+
let removeJwtSpy: jest.SpyInstance;
55+
let removeOtpSpy: jest.SpyInstance;
56+
57+
beforeEach(() => {
58+
removeJwtSpy = jest.spyOn(JwtTokenCache, 'removeToken');
59+
removeOtpSpy = jest.spyOn(MfaOtpRequestCache, 'remove');
60+
});
61+
62+
afterEach(() => {
63+
removeJwtSpy.mockReset();
64+
removeOtpSpy.mockReset();
65+
});
66+
67+
it('should clear the MFA and JWT cache if the error is an invalid JWT error', () => {
68+
const e = { code: 401, errno: 110 };
69+
70+
clearMfaAndJwtCacheOnInvalidJwt(e, scope);
71+
72+
expect(removeOtpSpy).toHaveBeenCalledWith(defaultSessionToken, scope);
73+
expect(removeJwtSpy).toHaveBeenCalledWith(defaultSessionToken, scope);
74+
});
75+
76+
it('should not clear the MFA and JWT cache if the error is not an invalid JWT error', () => {
77+
MfaOtpRequestCache.set(defaultSessionToken, scope);
78+
JwtTokenCache.setToken(defaultSessionToken, scope, jwt);
79+
const e = { code: 401, errno: 100 };
80+
81+
clearMfaAndJwtCacheOnInvalidJwt(e, scope);
82+
83+
expect(MfaOtpRequestCache.get(defaultSessionToken, scope)).toBeDefined();
84+
expect(JwtTokenCache.getToken(defaultSessionToken, scope)).toBeDefined();
85+
});
86+
87+
it('should not clear the MFA and JWT cache if the session token is not set', () => {
88+
MfaOtpRequestCache.set(defaultSessionToken, scope);
89+
JwtTokenCache.setToken(defaultSessionToken, scope, jwt);
90+
91+
// Override sessionToken to return undefined
92+
sessionTokenSpy.mockReturnValue(undefined);
93+
94+
const e = { code: 401, errno: 110 };
95+
96+
clearMfaAndJwtCacheOnInvalidJwt(e, scope);
97+
98+
expect(removeOtpSpy).not.toHaveBeenCalled();
99+
expect(removeJwtSpy).not.toHaveBeenCalled();
100+
});
101+
102+
it('should not clear the MFA and JWT cache if the session token is null', () => {
103+
MfaOtpRequestCache.set(defaultSessionToken, scope);
104+
JwtTokenCache.setToken(defaultSessionToken, scope, jwt);
105+
106+
// Override sessionToken to return null
107+
sessionTokenSpy.mockReturnValue(null);
108+
109+
const e = { code: 401, errno: 110 };
110+
111+
clearMfaAndJwtCacheOnInvalidJwt(e, scope);
112+
113+
expect(removeOtpSpy).not.toHaveBeenCalled();
114+
expect(removeJwtSpy).not.toHaveBeenCalled();
115+
});
116+
117+
it('does not throw if token does not exist by scope', () => {
118+
// Set a token in cache with a different scope
119+
JwtTokenCache.setToken(defaultSessionToken, 'email', jwt);
120+
121+
const e = { code: 401, errno: 110 };
122+
123+
expect(() => clearMfaAndJwtCacheOnInvalidJwt(e, scope)).not.toThrow();
124+
125+
expect(removeOtpSpy).toHaveBeenCalledWith(defaultSessionToken, scope);
126+
expect(removeJwtSpy).toHaveBeenCalledWith(defaultSessionToken, scope);
127+
});
128+
});
129+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import {
6+
JwtTokenCache,
7+
MfaOtpRequestCache,
8+
sessionToken as getSessionToken,
9+
} from './cache';
10+
import { MfaScope } from './types';
11+
12+
/**
13+
* Clears the MFA and JWT cache for the given scope if the error is an invalid JWT.
14+
*
15+
* Use this when checking the response from the auth server for an invalid JWT.
16+
* @param e - The error to check, must have code and errno properties.
17+
* @param scope - The scope to clear from the MFA and JWT cache.
18+
* @returns
19+
*/
20+
export const clearMfaAndJwtCacheOnInvalidJwt = (
21+
e: any,
22+
scope: MfaScope
23+
): void => {
24+
const sessionToken = getSessionToken();
25+
if (!sessionToken) {
26+
// noop - we can't do anything without a session token
27+
return;
28+
}
29+
if (isInvalidJwtError(e)) {
30+
MfaOtpRequestCache.remove(sessionToken, scope);
31+
JwtTokenCache.removeToken(sessionToken, scope);
32+
}
33+
};
34+
35+
/**
36+
* Checks if the error is an invalid JWT error.
37+
* @param e - The error to check, must have code and errno properties.
38+
* @returns
39+
*/
40+
export const isInvalidJwtError = (e: any): boolean => {
41+
return e && e.code === 401 && e.errno === 110;
42+
};

0 commit comments

Comments
 (0)