Merge pull request #19468 from mozilla/feat/mfa-guard-utils
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Glean probe-scraper / glean-probe-scraper (push) Waiting to run

chore(mfa): Add mfa helper utility for checking invalid JWT
This commit is contained in:
Nick Shirley 2025-09-17 13:55:40 -06:00 committed by GitHub
commit cabad89611
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 171 additions and 0 deletions

View File

@ -0,0 +1,129 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { JwtTokenCache, MfaOtpRequestCache } from './cache';
import {
clearMfaAndJwtCacheOnInvalidJwt,
isInvalidJwtError,
} from './mfa-guard-utils';
const defaultSessionToken = 'you-get-a-session-token';
const jwt = 'and-you-get-a-jwt';
const scope = 'test';
jest.mock('./cache', () => {
const actual = jest.requireActual('./cache');
return {
__esModule: true,
...actual,
sessionToken: jest.fn(() => defaultSessionToken),
};
});
describe('mfa-guard-utils', () => {
let sessionTokenSpy: jest.SpyInstance;
beforeEach(() => {
sessionTokenSpy = jest.mocked(require('./cache').sessionToken);
});
afterEach(() => {
sessionTokenSpy.mockReturnValue(defaultSessionToken);
});
describe('isInvalidJwtError', () => {
it('should return true if the error is an invalid JWT error', () => {
expect(isInvalidJwtError({ code: 401, errno: 110 })).toBe(true);
});
it('should return false if the error is not an invalid JWT error', () => {
expect(isInvalidJwtError({ code: 401, errno: 100 })).toBe(false);
});
it('should return false if the error is not an object', () => {
expect(isInvalidJwtError('not-an-object')).toBe(false);
});
it('should return false if the error is not an object with code and errno properties', () => {
expect(isInvalidJwtError({ code: 401 })).toBe(false);
});
});
describe('clearMfaAndJwtCacheOnInvalidJwt', () => {
let removeJwtSpy: jest.SpyInstance;
let removeOtpSpy: jest.SpyInstance;
beforeEach(() => {
removeJwtSpy = jest.spyOn(JwtTokenCache, 'removeToken');
removeOtpSpy = jest.spyOn(MfaOtpRequestCache, 'remove');
});
afterEach(() => {
removeJwtSpy.mockReset();
removeOtpSpy.mockReset();
});
it('should clear the MFA and JWT cache if the error is an invalid JWT error', () => {
const e = { code: 401, errno: 110 };
clearMfaAndJwtCacheOnInvalidJwt(e, scope);
expect(removeOtpSpy).toHaveBeenCalledWith(defaultSessionToken, scope);
expect(removeJwtSpy).toHaveBeenCalledWith(defaultSessionToken, scope);
});
it('should not clear the MFA and JWT cache if the error is not an invalid JWT error', () => {
MfaOtpRequestCache.set(defaultSessionToken, scope);
JwtTokenCache.setToken(defaultSessionToken, scope, jwt);
const e = { code: 401, errno: 100 };
clearMfaAndJwtCacheOnInvalidJwt(e, scope);
expect(MfaOtpRequestCache.get(defaultSessionToken, scope)).toBeDefined();
expect(JwtTokenCache.getToken(defaultSessionToken, scope)).toBeDefined();
});
it('should not clear the MFA and JWT cache if the session token is not set', () => {
MfaOtpRequestCache.set(defaultSessionToken, scope);
JwtTokenCache.setToken(defaultSessionToken, scope, jwt);
// Override sessionToken to return undefined
sessionTokenSpy.mockReturnValue(undefined);
const e = { code: 401, errno: 110 };
clearMfaAndJwtCacheOnInvalidJwt(e, scope);
expect(removeOtpSpy).not.toHaveBeenCalled();
expect(removeJwtSpy).not.toHaveBeenCalled();
});
it('should not clear the MFA and JWT cache if the session token is null', () => {
MfaOtpRequestCache.set(defaultSessionToken, scope);
JwtTokenCache.setToken(defaultSessionToken, scope, jwt);
// Override sessionToken to return null
sessionTokenSpy.mockReturnValue(null);
const e = { code: 401, errno: 110 };
clearMfaAndJwtCacheOnInvalidJwt(e, scope);
expect(removeOtpSpy).not.toHaveBeenCalled();
expect(removeJwtSpy).not.toHaveBeenCalled();
});
it('does not throw if token does not exist by scope', () => {
// Set a token in cache with a different scope
JwtTokenCache.setToken(defaultSessionToken, 'email', jwt);
const e = { code: 401, errno: 110 };
expect(() => clearMfaAndJwtCacheOnInvalidJwt(e, scope)).not.toThrow();
expect(removeOtpSpy).toHaveBeenCalledWith(defaultSessionToken, scope);
expect(removeJwtSpy).toHaveBeenCalledWith(defaultSessionToken, scope);
});
});
});

View File

@ -0,0 +1,42 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
JwtTokenCache,
MfaOtpRequestCache,
sessionToken as getSessionToken,
} from './cache';
import { MfaScope } from './types';
/**
* Clears the MFA and JWT cache for the given scope if the error is an invalid JWT.
*
* Use this when checking the response from the auth server for an invalid JWT.
* @param e - The error to check, must have code and errno properties.
* @param scope - The scope to clear from the MFA and JWT cache.
* @returns
*/
export const clearMfaAndJwtCacheOnInvalidJwt = (
e: any,
scope: MfaScope
): void => {
const sessionToken = getSessionToken();
if (!sessionToken) {
// noop - we can't do anything without a session token
return;
}
if (isInvalidJwtError(e)) {
MfaOtpRequestCache.remove(sessionToken, scope);
JwtTokenCache.removeToken(sessionToken, scope);
}
};
/**
* Checks if the error is an invalid JWT error.
* @param e - The error to check, must have code and errno properties.
* @returns
*/
export const isInvalidJwtError = (e: any): boolean => {
return e && e.code === 401 && e.errno === 110;
};