bitwarden-estensione-browser/libs/common/src/auth/services/token.service.spec.ts

2286 lines
83 KiB
TypeScript

import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom } from "rxjs";
import { FakeSingleUserStateProvider, FakeGlobalStateProvider } from "../../../spec";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { EncryptService } from "../../platform/abstractions/encrypt.service";
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
import { LogService } from "../../platform/abstractions/log.service";
import { AbstractStorageService } from "../../platform/abstractions/storage.service";
import { StorageLocation } from "../../platform/enums";
import { StorageOptions } from "../../platform/models/domain/storage-options";
import { UserId } from "../../types/guid";
import { ACCOUNT_ACTIVE_ACCOUNT_ID } from "./account.service";
import { DecodedAccessToken, TokenService } from "./token.service";
import {
ACCESS_TOKEN_DISK,
ACCESS_TOKEN_MEMORY,
API_KEY_CLIENT_ID_DISK,
API_KEY_CLIENT_ID_MEMORY,
API_KEY_CLIENT_SECRET_DISK,
API_KEY_CLIENT_SECRET_MEMORY,
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
REFRESH_TOKEN_DISK,
REFRESH_TOKEN_MEMORY,
SECURITY_STAMP_MEMORY,
} from "./token.state";
describe("TokenService", () => {
let tokenService: TokenService;
let singleUserStateProvider: FakeSingleUserStateProvider;
let globalStateProvider: FakeGlobalStateProvider;
let secureStorageService: MockProxy<AbstractStorageService>;
let keyGenerationService: MockProxy<KeyGenerationService>;
let encryptService: MockProxy<EncryptService>;
let logService: MockProxy<LogService>;
const memoryVaultTimeoutAction = VaultTimeoutAction.LogOut;
const memoryVaultTimeout = 30;
const diskVaultTimeoutAction = VaultTimeoutAction.Lock;
const diskVaultTimeout: number = null;
const accessTokenJwt =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0IiwibmJmIjoxNzA5MzI0MTExLCJpYXQiOjE3MDkzMjQxMTEsImV4cCI6MTcwOTMyNzcxMSwic2NvcGUiOlsiYXBpIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl0sImNsaWVudF9pZCI6IndlYiIsInN1YiI6ImVjZTcwYTEzLTcyMTYtNDNjNC05OTc3LWIxMDMwMTQ2ZTFlNyIsImF1dGhfdGltZSI6MTcwOTMyNDEwNCwiaWRwIjoiYml0d2FyZGVuIiwicHJlbWl1bSI6ZmFsc2UsImVtYWlsIjoiZXhhbXBsZUBiaXR3YXJkZW4uY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJzc3RhbXAiOiJHWTdKQU82NENLS1RLQkI2WkVBVVlMMldPUVU3QVNUMiIsIm5hbWUiOiJUZXN0IFVzZXIiLCJvcmdvd25lciI6WyI5MmI0OTkwOC1iNTE0LTQ1YTgtYmFkYi1iMTAzMDE0OGZlNTMiLCIzOGVkZTMyMi1iNGI0LTRiZDgtOWUwOS1iMTA3MDExMmRjMTEiLCJiMmQwNzAyOC1hNTgzLTRjM2UtOGQ2MC1iMTA3MDExOThjMjkiLCJiZjkzNGJhMi0wZmQ0LTQ5ZjItYTk1ZS1iMTA3MDExZmM5ZTYiLCJjMGI3Zjc1ZC0wMTVmLTQyYzktYjNhNi1iMTA4MDE3NjA3Y2EiXSwiZGV2aWNlIjoiNGI4NzIzNjctMGRhNi00MWEwLWFkY2ItNzdmMmZlZWZjNGY0IiwianRpIjoiNzUxNjFCRTQxMzFGRjVBMkRFNTExQjhDNEUyRkY4OUEifQ.n7roP8sSbfwcYdvRxZNZds27IK32TW6anorE6BORx_Q";
const accessTokenDecoded: DecodedAccessToken = {
iss: "http://localhost",
nbf: 1709324111,
iat: 1709324111,
exp: 1709327711,
scope: ["api", "offline_access"],
amr: ["Application"],
client_id: "web",
sub: "ece70a13-7216-43c4-9977-b1030146e1e7", // user id
auth_time: 1709324104,
idp: "bitwarden",
premium: false,
email: "example@bitwarden.com",
email_verified: false,
sstamp: "GY7JAO64CKKTKBB6ZEAUYL2WOQU7AST2",
name: "Test User",
orgowner: [
"92b49908-b514-45a8-badb-b1030148fe53",
"38ede322-b4b4-4bd8-9e09-b1070112dc11",
"b2d07028-a583-4c3e-8d60-b10701198c29",
"bf934ba2-0fd4-49f2-a95e-b107011fc9e6",
"c0b7f75d-015f-42c9-b3a6-b108017607ca",
],
device: "4b872367-0da6-41a0-adcb-77f2feefc4f4",
jti: "75161BE4131FF5A2DE511B8C4E2FF89A",
};
const userIdFromAccessToken: UserId = accessTokenDecoded.sub as UserId;
const secureStorageOptions: StorageOptions = {
storageLocation: StorageLocation.Disk,
useSecureStorage: true,
userId: userIdFromAccessToken,
};
const accessTokenKeyB64 = { keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8" };
beforeEach(() => {
jest.clearAllMocks();
singleUserStateProvider = new FakeSingleUserStateProvider();
globalStateProvider = new FakeGlobalStateProvider();
secureStorageService = mock<AbstractStorageService>();
keyGenerationService = mock<KeyGenerationService>();
encryptService = mock<EncryptService>();
logService = mock<LogService>();
const supportsSecureStorage = false; // default to false; tests will override as needed
tokenService = createTokenService(supportsSecureStorage);
});
it("instantiates", () => {
expect(tokenService).not.toBeFalsy();
});
describe("Access Token methods", () => {
const accessTokenKeyPartialSecureStorageKey = `_accessTokenKey`;
const accessTokenKeySecureStorageKey = `${userIdFromAccessToken}${accessTokenKeyPartialSecureStorageKey}`;
describe("hasAccessToken$", () => {
it("returns true when an access token exists in memory", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
// Act
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
// Assert
expect(result).toEqual(true);
});
it("returns true when an access token exists in disk", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
// Act
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
// Assert
expect(result).toEqual(true);
});
it("returns true when an access token exists in secure storage", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]);
secureStorageService.get.mockResolvedValue(accessTokenKeyB64);
// Act
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
// Assert
expect(result).toEqual(true);
});
it("should return false if no access token exists in memory, disk, or secure storage", async () => {
// Act
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
// Assert
expect(result).toEqual(false);
});
});
describe("setAccessToken", () => {
it("should throw an error if the access token is null", async () => {
// Act
const result = tokenService.setAccessToken(null, VaultTimeoutAction.Lock, null);
// Assert
await expect(result).rejects.toThrow("Access token is required.");
});
it("should throw an error if an invalid token is passed in", async () => {
// Act
const result = tokenService.setAccessToken("invalidToken", VaultTimeoutAction.Lock, null);
// Assert
await expect(result).rejects.toThrow("JWT must have 3 parts");
});
it("should not throw an error as long as the token is valid", async () => {
// Act
const result = tokenService.setAccessToken(accessTokenJwt, VaultTimeoutAction.Lock, null);
// Assert
await expect(result).resolves.not.toThrow();
});
describe("Memory storage tests", () => {
it("should set the access token in memory", async () => {
// Act
await tokenService.setAccessToken(
accessTokenJwt,
memoryVaultTimeoutAction,
memoryVaultTimeout,
);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY).nextMock,
).toHaveBeenCalledWith(accessTokenJwt);
});
});
describe("Disk storage tests (secure storage not supported on platform)", () => {
it("should set the access token in disk", async () => {
// Act
await tokenService.setAccessToken(
accessTokenJwt,
diskVaultTimeoutAction,
diskVaultTimeout,
);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK).nextMock,
).toHaveBeenCalledWith(accessTokenJwt);
});
});
describe("Disk storage tests (secure storage supported on platform)", () => {
beforeEach(() => {
const supportsSecureStorage = true;
tokenService = createTokenService(supportsSecureStorage);
});
it("should set an access token key in secure storage, the encrypted access token in disk, and clear out the token in memory", async () => {
// Arrange:
// For testing purposes, let's assume that the access token is already in memory
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
keyGenerationService.createKey.mockResolvedValue("accessTokenKey" as any);
const mockEncryptedAccessToken = "encryptedAccessToken";
encryptService.encrypt.mockResolvedValue({
encryptedString: mockEncryptedAccessToken,
} as any);
// Act
await tokenService.setAccessToken(
accessTokenJwt,
diskVaultTimeoutAction,
diskVaultTimeout,
);
// Assert
// assert that the AccessTokenKey was set in secure storage
expect(secureStorageService.save).toHaveBeenCalledWith(
accessTokenKeySecureStorageKey,
"accessTokenKey",
secureStorageOptions,
);
// assert that the access token was encrypted and set in disk
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK).nextMock,
).toHaveBeenCalledWith(mockEncryptedAccessToken);
// assert data was migrated out of memory
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY).nextMock,
).toHaveBeenCalledWith(null);
});
});
});
describe("getAccessToken", () => {
it("should return undefined if no user id is provided and there is no active user in global state", async () => {
// Act
const result = await tokenService.getAccessToken();
// Assert
expect(result).toBeUndefined();
});
it("should return null if no access token is found in memory, disk, or secure storage", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await tokenService.getAccessToken();
// Assert
expect(result).toBeNull();
});
describe("Memory storage tests", () => {
test.each([
[
"should get the access token from memory for the provided user id",
userIdFromAccessToken,
],
["should get the access token from memory with no user id provided", undefined],
])("%s", async (_, userId) => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
// Need to have global active id set to the user id
if (!userId) {
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
}
// Act
const result = await tokenService.getAccessToken(userId);
// Assert
expect(result).toEqual(accessTokenJwt);
});
});
describe("Disk storage tests (secure storage not supported on platform)", () => {
test.each([
[
"should get the access token from disk for the specified user id",
userIdFromAccessToken,
],
["should get the access token from disk with no user id specified", undefined],
])("%s", async (_, userId) => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
// Need to have global active id set to the user id
if (!userId) {
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
}
// Act
const result = await tokenService.getAccessToken(userId);
// Assert
expect(result).toEqual(accessTokenJwt);
});
});
describe("Disk storage tests (secure storage supported on platform)", () => {
beforeEach(() => {
const supportsSecureStorage = true;
tokenService = createTokenService(supportsSecureStorage);
});
test.each([
[
"should get the encrypted access token from disk, decrypt it, and return it when user id is provided",
userIdFromAccessToken,
],
[
"should get the encrypted access token from disk, decrypt it, and return it when no user id is provided",
undefined,
],
])("%s", async (_, userId) => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]);
secureStorageService.get.mockResolvedValue(accessTokenKeyB64);
encryptService.decryptToUtf8.mockResolvedValue("decryptedAccessToken");
// Need to have global active id set to the user id
if (!userId) {
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
}
// Act
const result = await tokenService.getAccessToken(userId);
// Assert
expect(result).toEqual("decryptedAccessToken");
});
test.each([
[
"should fallback and get the unencrypted access token from disk when there isn't an access token key in secure storage and a user id is provided",
userIdFromAccessToken,
],
[
"should fallback and get the unencrypted access token from disk when there isn't an access token key in secure storage and no user id is provided",
undefined,
],
])("%s", async (_, userId) => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
// Need to have global active id set to the user id
if (!userId) {
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
}
// No access token key set
// Act
const result = await tokenService.getAccessToken(userId);
// Assert
expect(result).toEqual(accessTokenJwt);
});
});
});
describe("clearAccessToken", () => {
it("should throw an error if no user id is provided and there is no active user in global state", async () => {
// Act
// note: don't await here because we want to test the error
const result = tokenService.clearAccessToken();
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot clear access token.");
});
describe("Secure storage enabled", () => {
beforeEach(() => {
const supportsSecureStorage = true;
tokenService = createTokenService(supportsSecureStorage);
});
test.each([
[
"should clear the access token from all storage locations for the provided user id",
userIdFromAccessToken,
],
[
"should clear the access token from all storage locations for the global active user",
undefined,
],
])("%s", async (_, userId) => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
singleUserStateProvider
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
// Need to have global active id set to the user id
if (!userId) {
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
}
// Act
await tokenService.clearAccessToken(userIdFromAccessToken);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY).nextMock,
).toHaveBeenCalledWith(null);
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK).nextMock,
).toHaveBeenCalledWith(null);
expect(secureStorageService.remove).toHaveBeenCalledWith(
accessTokenKeySecureStorageKey,
secureStorageOptions,
);
});
});
});
describe("decodeAccessToken", () => {
it("should throw an error if no access token provided or retrieved from state", async () => {
// Access
tokenService.getAccessToken = jest.fn().mockResolvedValue(null);
// Act
// note: don't await here because we want to test the error
const result = tokenService.decodeAccessToken();
// Assert
await expect(result).rejects.toThrow("Access token not found.");
});
it("should decode the access token", async () => {
// Arrange
tokenService.getAccessToken = jest.fn().mockResolvedValue(accessTokenJwt);
// Act
const result = await tokenService.decodeAccessToken();
// Assert
expect(result).toEqual(accessTokenDecoded);
});
});
describe("Data methods", () => {
describe("getTokenExpirationDate", () => {
it("should throw an error if the access token cannot be decoded", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error"));
// Act
// note: don't await here because we want to test the error
const result = tokenService.getTokenExpirationDate();
// Assert
await expect(result).rejects.toThrow("Failed to decode access token: Mock error");
});
it("should return null if the decoded access token is null", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null);
// Act
const result = await tokenService.getTokenExpirationDate();
// Assert
expect(result).toBeNull();
});
it("should return null if the decoded access token does not have an expiration date", async () => {
// Arrange
const accessTokenDecodedWithoutExp = { ...accessTokenDecoded };
delete accessTokenDecodedWithoutExp.exp;
tokenService.decodeAccessToken = jest
.fn()
.mockResolvedValue(accessTokenDecodedWithoutExp);
// Act
const result = await tokenService.getTokenExpirationDate();
// Assert
expect(result).toBeNull();
});
it("should return null if the decoded access token has an non numeric expiration date", async () => {
// Arrange
const accessTokenDecodedWithNonNumericExp = { ...accessTokenDecoded, exp: "non-numeric" };
tokenService.decodeAccessToken = jest
.fn()
.mockResolvedValue(accessTokenDecodedWithNonNumericExp);
// Act
const result = await tokenService.getTokenExpirationDate();
// Assert
expect(result).toBeNull();
});
it("should return the expiration date of the access token", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded);
// Act
const result = await tokenService.getTokenExpirationDate();
// Assert
expect(result).toEqual(new Date(accessTokenDecoded.exp * 1000));
});
});
describe("tokenSecondsRemaining", () => {
it("should return 0 if the tokenExpirationDate is null", async () => {
// Arrange
tokenService.getTokenExpirationDate = jest.fn().mockResolvedValue(null);
// Act
const result = await tokenService.tokenSecondsRemaining();
// Assert
expect(result).toEqual(0);
});
it("should return the number of seconds remaining until the token expires", async () => {
// Arrange
// Lock the time to ensure a consistent test environment
// otherwise we have flaky issues with set system time date and the Date.now() call.
const fixedCurrentTime = new Date("2024-03-06T00:00:00Z");
jest.useFakeTimers().setSystemTime(fixedCurrentTime);
const nowInSeconds = Math.floor(Date.now() / 1000);
const expirationInSeconds = nowInSeconds + 3600; // token expires in 1 hr
const expectedSecondsRemaining = expirationInSeconds - nowInSeconds;
const expirationDate = new Date(0);
expirationDate.setUTCSeconds(expirationInSeconds);
tokenService.getTokenExpirationDate = jest.fn().mockResolvedValue(expirationDate);
// Act
const result = await tokenService.tokenSecondsRemaining();
// Assert
expect(result).toEqual(expectedSecondsRemaining);
// Reset the timers to be the real ones
jest.useRealTimers();
});
it("should return the number of seconds remaining until the token expires, considering an offset", async () => {
// Arrange
// Lock the time to ensure a consistent test environment
// otherwise we have flaky issues with set system time date and the Date.now() call.
const fixedCurrentTime = new Date("2024-03-06T00:00:00Z");
jest.useFakeTimers().setSystemTime(fixedCurrentTime);
const nowInSeconds = Math.floor(Date.now() / 1000);
const offsetSeconds = 300; // 5 minute offset
const expirationInSeconds = nowInSeconds + 3600; // token expires in 1 hr
const expectedSecondsRemaining = expirationInSeconds - nowInSeconds - offsetSeconds; // Adjust for offset
const expirationDate = new Date(0);
expirationDate.setUTCSeconds(expirationInSeconds);
tokenService.getTokenExpirationDate = jest.fn().mockResolvedValue(expirationDate);
// Act
const result = await tokenService.tokenSecondsRemaining(offsetSeconds);
// Assert
expect(result).toEqual(expectedSecondsRemaining);
// Reset the timers to be the real ones
jest.useRealTimers();
});
});
describe("tokenNeedsRefresh", () => {
it("should return true if token is within the default refresh threshold (5 min)", async () => {
// Arrange
const tokenSecondsRemaining = 60;
tokenService.tokenSecondsRemaining = jest.fn().mockResolvedValue(tokenSecondsRemaining);
// Act
const result = await tokenService.tokenNeedsRefresh();
// Assert
expect(result).toEqual(true);
});
it("should return false if token is outside the default refresh threshold (5 min)", async () => {
// Arrange
const tokenSecondsRemaining = 600;
tokenService.tokenSecondsRemaining = jest.fn().mockResolvedValue(tokenSecondsRemaining);
// Act
const result = await tokenService.tokenNeedsRefresh();
// Assert
expect(result).toEqual(false);
});
it("should return true if token is within the specified refresh threshold", async () => {
// Arrange
const tokenSecondsRemaining = 60;
tokenService.tokenSecondsRemaining = jest.fn().mockResolvedValue(tokenSecondsRemaining);
// Act
const result = await tokenService.tokenNeedsRefresh(2);
// Assert
expect(result).toEqual(true);
});
it("should return false if token is outside the specified refresh threshold", async () => {
// Arrange
const tokenSecondsRemaining = 600;
tokenService.tokenSecondsRemaining = jest.fn().mockResolvedValue(tokenSecondsRemaining);
// Act
const result = await tokenService.tokenNeedsRefresh(5);
// Assert
expect(result).toEqual(false);
});
});
describe("getUserId", () => {
it("should throw an error if the access token cannot be decoded", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error"));
// Act
// note: don't await here because we want to test the error
const result = tokenService.getUserId();
// Assert
await expect(result).rejects.toThrow("Failed to decode access token: Mock error");
});
it("should throw an error if the decoded access token is null", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null);
// Act
// note: don't await here because we want to test the error
const result = tokenService.getUserId();
// Assert
await expect(result).rejects.toThrow("No user id found");
});
it("should throw an error if the decoded access token has a non-string user id", async () => {
// Arrange
const accessTokenDecodedWithNonStringSub = { ...accessTokenDecoded, sub: 123 };
tokenService.decodeAccessToken = jest
.fn()
.mockResolvedValue(accessTokenDecodedWithNonStringSub);
// Act
// note: don't await here because we want to test the error
const result = tokenService.getUserId();
// Assert
await expect(result).rejects.toThrow("No user id found");
});
it("should return the user id from the decoded access token", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded);
// Act
const result = await tokenService.getUserId();
// Assert
expect(result).toEqual(userIdFromAccessToken);
});
});
describe("getUserIdFromAccessToken", () => {
it("should throw an error if the access token cannot be decoded", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error"));
// Act
// note: don't await here because we want to test the error
const result = (tokenService as any).getUserIdFromAccessToken(accessTokenJwt);
// Assert
await expect(result).rejects.toThrow("Failed to decode access token: Mock error");
});
it("should throw an error if the decoded access token is null", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null);
// Act
// note: don't await here because we want to test the error
const result = (tokenService as any).getUserIdFromAccessToken(accessTokenJwt);
// Assert
await expect(result).rejects.toThrow("No user id found");
});
it("should throw an error if the decoded access token has a non-string user id", async () => {
// Arrange
const accessTokenDecodedWithNonStringSub = { ...accessTokenDecoded, sub: 123 };
tokenService.decodeAccessToken = jest
.fn()
.mockResolvedValue(accessTokenDecodedWithNonStringSub);
// Act
// note: don't await here because we want to test the error
const result = (tokenService as any).getUserIdFromAccessToken(accessTokenJwt);
// Assert
await expect(result).rejects.toThrow("No user id found");
});
it("should return the user id from the decoded access token", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded);
// Act
const result = await (tokenService as any).getUserIdFromAccessToken(accessTokenJwt);
// Assert
expect(result).toEqual(userIdFromAccessToken);
});
});
describe("getEmail", () => {
it("should throw an error if the access token cannot be decoded", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error"));
// Act
// note: don't await here because we want to test the error
const result = tokenService.getEmail();
// Assert
await expect(result).rejects.toThrow("Failed to decode access token: Mock error");
});
it("should throw an error if the decoded access token is null", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null);
// Act
// note: don't await here because we want to test the error
const result = tokenService.getEmail();
// Assert
await expect(result).rejects.toThrow("No email found");
});
it("should throw an error if the decoded access token has a non-string email", async () => {
// Arrange
const accessTokenDecodedWithNonStringEmail = { ...accessTokenDecoded, email: 123 };
tokenService.decodeAccessToken = jest
.fn()
.mockResolvedValue(accessTokenDecodedWithNonStringEmail);
// Act
// note: don't await here because we want to test the error
const result = tokenService.getEmail();
// Assert
await expect(result).rejects.toThrow("No email found");
});
it("should return the email from the decoded access token", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded);
// Act
const result = await tokenService.getEmail();
// Assert
expect(result).toEqual(accessTokenDecoded.email);
});
});
describe("getEmailVerified", () => {
it("should throw an error if the access token cannot be decoded", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error"));
// Act
// note: don't await here because we want to test the error
const result = tokenService.getEmailVerified();
// Assert
await expect(result).rejects.toThrow("Failed to decode access token: Mock error");
});
it("should throw an error if the decoded access token is null", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null);
// Act
// note: don't await here because we want to test the error
const result = tokenService.getEmailVerified();
// Assert
await expect(result).rejects.toThrow("No email verification found");
});
it("should throw an error if the decoded access token has a non-boolean email_verified", async () => {
// Arrange
const accessTokenDecodedWithNonBooleanEmailVerified = {
...accessTokenDecoded,
email_verified: 123,
};
tokenService.decodeAccessToken = jest
.fn()
.mockResolvedValue(accessTokenDecodedWithNonBooleanEmailVerified);
// Act
// note: don't await here because we want to test the error
const result = tokenService.getEmailVerified();
// Assert
await expect(result).rejects.toThrow("No email verification found");
});
it("should return the email_verified from the decoded access token", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded);
// Act
const result = await tokenService.getEmailVerified();
// Assert
expect(result).toEqual(accessTokenDecoded.email_verified);
});
});
describe("getName", () => {
it("should throw an error if the access token cannot be decoded", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error"));
// Act
// note: don't await here because we want to test the error
const result = tokenService.getName();
// Assert
await expect(result).rejects.toThrow("Failed to decode access token: Mock error");
});
it("should return null if the decoded access token is null", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null);
// Act
const result = await tokenService.getName();
// Assert
expect(result).toBeNull();
});
it("should return null if the decoded access token has a non-string name", async () => {
// Arrange
const accessTokenDecodedWithNonStringName = { ...accessTokenDecoded, name: 123 };
tokenService.decodeAccessToken = jest
.fn()
.mockResolvedValue(accessTokenDecodedWithNonStringName);
// Act
const result = await tokenService.getName();
// Assert
expect(result).toBeNull();
});
it("should return the name from the decoded access token", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded);
// Act
const result = await tokenService.getName();
// Assert
expect(result).toEqual(accessTokenDecoded.name);
});
});
describe("getIssuer", () => {
it("should throw an error if the access token cannot be decoded", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error"));
// Act
// note: don't await here because we want to test the error
const result = tokenService.getIssuer();
// Assert
await expect(result).rejects.toThrow("Failed to decode access token: Mock error");
});
it("should throw an error if the decoded access token is null", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(null);
// Act
// note: don't await here because we want to test the error
const result = tokenService.getIssuer();
// Assert
await expect(result).rejects.toThrow("No issuer found");
});
it("should throw an error if the decoded access token has a non-string iss", async () => {
// Arrange
const accessTokenDecodedWithNonStringIss = { ...accessTokenDecoded, iss: 123 };
tokenService.decodeAccessToken = jest
.fn()
.mockResolvedValue(accessTokenDecodedWithNonStringIss);
// Act
// note: don't await here because we want to test the error
const result = tokenService.getIssuer();
// Assert
await expect(result).rejects.toThrow("No issuer found");
});
it("should return the issuer from the decoded access token", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockResolvedValue(accessTokenDecoded);
// Act
const result = await tokenService.getIssuer();
// Assert
expect(result).toEqual(accessTokenDecoded.iss);
});
});
describe("getIsExternal", () => {
it("should throw an error if the access token cannot be decoded", async () => {
// Arrange
tokenService.decodeAccessToken = jest.fn().mockRejectedValue(new Error("Mock error"));
// Act
// note: don't await here because we want to test the error
const result = tokenService.getIsExternal();
// Assert
await expect(result).rejects.toThrow("Failed to decode access token: Mock error");
});
it("should return false if the amr (Authentication Method Reference) claim does not contain 'external'", async () => {
// Arrange
const accessTokenDecodedWithoutExternalAmr = {
...accessTokenDecoded,
amr: ["not-external"],
};
tokenService.decodeAccessToken = jest
.fn()
.mockResolvedValue(accessTokenDecodedWithoutExternalAmr);
// Act
const result = await tokenService.getIsExternal();
// Assert
expect(result).toEqual(false);
});
it("should return true if the amr (Authentication Method Reference) claim contains 'external'", async () => {
// Arrange
const accessTokenDecodedWithExternalAmr = {
...accessTokenDecoded,
amr: ["external"],
};
tokenService.decodeAccessToken = jest
.fn()
.mockResolvedValue(accessTokenDecodedWithExternalAmr);
// Act
const result = await tokenService.getIsExternal();
// Assert
expect(result).toEqual(true);
});
});
});
});
describe("Refresh Token methods", () => {
const refreshToken = "refreshToken";
const refreshTokenPartialSecureStorageKey = `_refreshToken`;
const refreshTokenSecureStorageKey = `${userIdFromAccessToken}${refreshTokenPartialSecureStorageKey}`;
describe("setRefreshToken", () => {
it("should throw an error if no user id is provided", async () => {
// Act
// note: don't await here because we want to test the error
const result = (tokenService as any).setRefreshToken(
refreshToken,
VaultTimeoutAction.Lock,
null,
null,
);
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot save refresh token.");
});
describe("Memory storage tests", () => {
it("should set the refresh token in memory for the specified user id", async () => {
// Act
await (tokenService as any).setRefreshToken(
refreshToken,
memoryVaultTimeoutAction,
memoryVaultTimeout,
userIdFromAccessToken,
);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY).nextMock,
).toHaveBeenCalledWith(refreshToken);
});
});
describe("Disk storage tests (secure storage not supported on platform)", () => {
it("should set the refresh token in disk for the specified user id", async () => {
// Act
await (tokenService as any).setRefreshToken(
refreshToken,
diskVaultTimeoutAction,
diskVaultTimeout,
userIdFromAccessToken,
);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK).nextMock,
).toHaveBeenCalledWith(refreshToken);
});
});
describe("Disk storage tests (secure storage supported on platform)", () => {
beforeEach(() => {
const supportsSecureStorage = true;
tokenService = createTokenService(supportsSecureStorage);
});
it("should set the refresh token in secure storage, null out data on disk or in memory, and set a flag to indicate the token has been migrated for the specified user id", async () => {
// Arrange:
// For testing purposes, let's assume that the token is already in disk and memory
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
// Act
await (tokenService as any).setRefreshToken(
refreshToken,
diskVaultTimeoutAction,
diskVaultTimeout,
userIdFromAccessToken,
);
// Assert
// assert that the refresh token was set in secure storage
expect(secureStorageService.save).toHaveBeenCalledWith(
refreshTokenSecureStorageKey,
refreshToken,
secureStorageOptions,
);
// assert data was migrated out of disk and memory
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK).nextMock,
).toHaveBeenCalledWith(null);
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY).nextMock,
).toHaveBeenCalledWith(null);
});
});
});
describe("getRefreshToken", () => {
it("should return undefined if no user id is provided and there is no active user in global state", async () => {
// Act
const result = await (tokenService as any).getRefreshToken();
// Assert
expect(result).toBeUndefined();
});
it("should return null if no refresh token is found in memory, disk, or secure storage", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await (tokenService as any).getRefreshToken();
// Assert
expect(result).toBeNull();
});
describe("Memory storage tests", () => {
it("should get the refresh token from memory with no user id specified (uses global active user)", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
// Assert
expect(result).toEqual(refreshToken);
});
it("should get the refresh token from memory for the specified user id", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
// Assert
expect(result).toEqual(refreshToken);
});
});
describe("Disk storage tests (secure storage not supported on platform)", () => {
it("should get the refresh token from disk with no user id specified", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
// Assert
expect(result).toEqual(refreshToken);
});
it("should get the refresh token from disk for the specified user id", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
// Assert
expect(result).toEqual(refreshToken);
});
});
describe("Disk storage tests (secure storage supported on platform)", () => {
beforeEach(() => {
const supportsSecureStorage = true;
tokenService = createTokenService(supportsSecureStorage);
});
it("should get the refresh token from secure storage when no user id is specified and the migration flag is set to true", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
secureStorageService.get.mockResolvedValue(refreshToken);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
// Assert
expect(result).toEqual(refreshToken);
});
it("should get the refresh token from secure storage when user id is specified and the migration flag set to true", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
secureStorageService.get.mockResolvedValue(refreshToken);
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
// Assert
expect(result).toEqual(refreshToken);
});
it("should fallback and get the refresh token from disk when user id is specified and the migration flag is set to false even if the platform supports secure storage", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
// Act
const result = await tokenService.getRefreshToken(userIdFromAccessToken);
// Assert
expect(result).toEqual(refreshToken);
// assert that secure storage was not called
expect(secureStorageService.get).not.toHaveBeenCalled();
});
it("should fallback and get the refresh token from disk when no user id is specified and the migration flag is set to false even if the platform supports secure storage", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await tokenService.getRefreshToken();
// Assert
expect(result).toEqual(refreshToken);
// assert that secure storage was not called
expect(secureStorageService.get).not.toHaveBeenCalled();
});
});
});
describe("clearRefreshToken", () => {
it("should throw an error if no user id is provided", async () => {
// Act
// note: don't await here because we want to test the error
const result = (tokenService as any).clearRefreshToken();
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot clear refresh token.");
});
describe("Secure storage enabled", () => {
beforeEach(() => {
const supportsSecureStorage = true;
tokenService = createTokenService(supportsSecureStorage);
});
it("should clear the refresh token from all storage locations for the specified user id", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
singleUserStateProvider
.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK)
.stateSubject.next([userIdFromAccessToken, refreshToken]);
// Act
await (tokenService as any).clearRefreshToken(userIdFromAccessToken);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_MEMORY).nextMock,
).toHaveBeenCalledWith(null);
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, REFRESH_TOKEN_DISK).nextMock,
).toHaveBeenCalledWith(null);
expect(secureStorageService.remove).toHaveBeenCalledWith(
refreshTokenSecureStorageKey,
secureStorageOptions,
);
});
});
});
});
describe("Client Id methods", () => {
const clientId = "clientId";
describe("setClientId", () => {
it("should throw an error if no user id is provided and there is no active user in global state", async () => {
// Act
// note: don't await here because we want to test the error
const result = tokenService.setClientId(clientId, VaultTimeoutAction.Lock, null);
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot save client id.");
});
describe("Memory storage tests", () => {
it("should set the client id in memory when there is an active user in global state", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
await tokenService.setClientId(clientId, memoryVaultTimeoutAction, memoryVaultTimeout);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.nextMock,
).toHaveBeenCalledWith(clientId);
});
it("should set the client id in memory for the specified user id", async () => {
// Act
await tokenService.setClientId(
clientId,
memoryVaultTimeoutAction,
memoryVaultTimeout,
userIdFromAccessToken,
);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.nextMock,
).toHaveBeenCalledWith(clientId);
});
});
describe("Disk storage tests", () => {
it("should set the client id in disk when there is an active user in global state", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
await tokenService.setClientId(clientId, diskVaultTimeoutAction, diskVaultTimeout);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK).nextMock,
).toHaveBeenCalledWith(clientId);
});
it("should set the client id in disk for the specified user id", async () => {
// Act
await tokenService.setClientId(
clientId,
diskVaultTimeoutAction,
diskVaultTimeout,
userIdFromAccessToken,
);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK).nextMock,
).toHaveBeenCalledWith(clientId);
});
});
});
describe("getClientId", () => {
it("should return undefined if no user id is provided and there is no active user in global state", async () => {
// Act
const result = await tokenService.getClientId();
// Assert
expect(result).toBeUndefined();
});
it("should return null if no client id is found in memory or disk", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await tokenService.getClientId();
// Assert
expect(result).toBeNull();
});
describe("Memory storage tests", () => {
it("should get the client id from memory with no user id specified (uses global active user)", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientId]);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await tokenService.getClientId();
// Assert
expect(result).toEqual(clientId);
});
it("should get the client id from memory for the specified user id", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientId]);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
// Act
const result = await tokenService.getClientId(userIdFromAccessToken);
// Assert
expect(result).toEqual(clientId);
});
});
describe("Disk storage tests", () => {
it("should get the client id from disk with no user id specified", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, clientId]);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await tokenService.getClientId();
// Assert
expect(result).toEqual(clientId);
});
it("should get the client id from disk for the specified user id", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, clientId]);
// Act
const result = await tokenService.getClientId(userIdFromAccessToken);
// Assert
expect(result).toEqual(clientId);
});
});
});
describe("clearClientId", () => {
it("should throw an error if no user id is provided and there is no active user in global state", async () => {
// Act
// note: don't await here because we want to test the error
const result = (tokenService as any).clearClientId();
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot clear client id.");
});
it("should clear the client id from memory and disk for the specified user id", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientId]);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, clientId]);
// Act
await (tokenService as any).clearClientId(userIdFromAccessToken);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY).nextMock,
).toHaveBeenCalledWith(null);
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK).nextMock,
).toHaveBeenCalledWith(null);
});
it("should clear the client id from memory and disk for the global active user", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientId]);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK)
.stateSubject.next([userIdFromAccessToken, clientId]);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
await (tokenService as any).clearClientId();
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_MEMORY).nextMock,
).toHaveBeenCalledWith(null);
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_ID_DISK).nextMock,
).toHaveBeenCalledWith(null);
});
});
});
describe("Client Secret methods", () => {
const clientSecret = "clientSecret";
describe("setClientSecret", () => {
it("should throw an error if no user id is provided and there is no active user in global state", async () => {
// Act
// note: don't await here because we want to test the error
const result = tokenService.setClientSecret(clientSecret, VaultTimeoutAction.Lock, null);
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot save client secret.");
});
describe("Memory storage tests", () => {
it("should set the client secret in memory when there is an active user in global state", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
await tokenService.setClientSecret(
clientSecret,
memoryVaultTimeoutAction,
memoryVaultTimeout,
);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.nextMock,
).toHaveBeenCalledWith(clientSecret);
});
it("should set the client secret in memory for the specified user id", async () => {
// Act
await tokenService.setClientSecret(
clientSecret,
memoryVaultTimeoutAction,
memoryVaultTimeout,
userIdFromAccessToken,
);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.nextMock,
).toHaveBeenCalledWith(clientSecret);
});
});
describe("Disk storage tests", () => {
it("should set the client secret in disk when there is an active user in global state", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
await tokenService.setClientSecret(
clientSecret,
diskVaultTimeoutAction,
diskVaultTimeout,
);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.nextMock,
).toHaveBeenCalledWith(clientSecret);
});
it("should set the client secret in disk for the specified user id", async () => {
// Act
await tokenService.setClientSecret(
clientSecret,
diskVaultTimeoutAction,
diskVaultTimeout,
userIdFromAccessToken,
);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.nextMock,
).toHaveBeenCalledWith(clientSecret);
});
});
});
describe("getClientSecret", () => {
it("should return undefined if no user id is provided and there is no active user in global state", async () => {
// Act
const result = await tokenService.getClientSecret();
// Assert
expect(result).toBeUndefined();
});
it("should return null if no client secret is found in memory or disk", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await tokenService.getClientSecret();
// Assert
expect(result).toBeNull();
});
describe("Memory storage tests", () => {
it("should get the client secret from memory with no user id specified (uses global active user)", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await tokenService.getClientSecret();
// Assert
expect(result).toEqual(clientSecret);
});
it("should get the client secret from memory for the specified user id", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
// set disk to undefined
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, undefined]);
// Act
const result = await tokenService.getClientSecret(userIdFromAccessToken);
// Assert
expect(result).toEqual(clientSecret);
});
});
describe("Disk storage tests", () => {
it("should get the client secret from disk with no user id specified", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
const result = await tokenService.getClientSecret();
// Assert
expect(result).toEqual(clientSecret);
});
it("should get the client secret from disk for the specified user id", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, undefined]);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
// Act
const result = await tokenService.getClientSecret(userIdFromAccessToken);
// Assert
expect(result).toEqual(clientSecret);
});
});
});
describe("clearClientSecret", () => {
it("should throw an error if no user id is provided and there is no active user in global state", async () => {
// Act
// note: don't await here because we want to test the error
const result = (tokenService as any).clearClientSecret();
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot clear client secret.");
});
it("should clear the client secret from memory and disk for the specified user id", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
// Act
await (tokenService as any).clearClientSecret(userIdFromAccessToken);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.nextMock,
).toHaveBeenCalledWith(null);
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.nextMock,
).toHaveBeenCalledWith(null);
});
it("should clear the client secret from memory and disk for the global active user", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
singleUserStateProvider
.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.stateSubject.next([userIdFromAccessToken, clientSecret]);
// Need to have global active id set to the user id
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
await (tokenService as any).clearClientSecret();
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_MEMORY)
.nextMock,
).toHaveBeenCalledWith(null);
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, API_KEY_CLIENT_SECRET_DISK)
.nextMock,
).toHaveBeenCalledWith(null);
});
});
});
describe("setTokens", () => {
it("should call to set all passed in tokens after deriving user id from the access token", async () => {
// Arrange
const refreshToken = "refreshToken";
// specific vault timeout actions and vault timeouts don't change this test so values don't matter.
const vaultTimeoutAction = VaultTimeoutAction.Lock;
const vaultTimeout = 30;
const clientId = "clientId";
const clientSecret = "clientSecret";
(tokenService as any)._setAccessToken = jest.fn();
// any hack allows for mocking private method.
(tokenService as any).setRefreshToken = jest.fn();
tokenService.setClientId = jest.fn();
tokenService.setClientSecret = jest.fn();
// Act
// Note: passing a valid access token so that a valid user id can be determined from the access token
await tokenService.setTokens(accessTokenJwt, vaultTimeoutAction, vaultTimeout, refreshToken, [
clientId,
clientSecret,
]);
// Assert
expect((tokenService as any)._setAccessToken).toHaveBeenCalledWith(
accessTokenJwt,
vaultTimeoutAction,
vaultTimeout,
userIdFromAccessToken,
);
// any hack allows for testing private methods
expect((tokenService as any).setRefreshToken).toHaveBeenCalledWith(
refreshToken,
vaultTimeoutAction,
vaultTimeout,
userIdFromAccessToken,
);
expect(tokenService.setClientId).toHaveBeenCalledWith(
clientId,
vaultTimeoutAction,
vaultTimeout,
userIdFromAccessToken,
);
expect(tokenService.setClientSecret).toHaveBeenCalledWith(
clientSecret,
vaultTimeoutAction,
vaultTimeout,
userIdFromAccessToken,
);
});
it("should not try to set client id and client secret if they are not passed in", async () => {
// Arrange
const refreshToken = "refreshToken";
const vaultTimeoutAction = VaultTimeoutAction.Lock;
const vaultTimeout = 30;
(tokenService as any)._setAccessToken = jest.fn();
(tokenService as any).setRefreshToken = jest.fn();
tokenService.setClientId = jest.fn();
tokenService.setClientSecret = jest.fn();
// Act
await tokenService.setTokens(accessTokenJwt, vaultTimeoutAction, vaultTimeout, refreshToken);
// Assert
expect((tokenService as any)._setAccessToken).toHaveBeenCalledWith(
accessTokenJwt,
vaultTimeoutAction,
vaultTimeout,
userIdFromAccessToken,
);
// any hack allows for testing private methods
expect((tokenService as any).setRefreshToken).toHaveBeenCalledWith(
refreshToken,
vaultTimeoutAction,
vaultTimeout,
userIdFromAccessToken,
);
expect(tokenService.setClientId).not.toHaveBeenCalled();
expect(tokenService.setClientSecret).not.toHaveBeenCalled();
});
it("should throw an error if the access token is invalid", async () => {
// Arrange
const accessToken = "invalidToken";
const refreshToken = "refreshToken";
const vaultTimeoutAction = VaultTimeoutAction.Lock;
const vaultTimeout = 30;
// Act
const result = tokenService.setTokens(
accessToken,
vaultTimeoutAction,
vaultTimeout,
refreshToken,
);
// Assert
await expect(result).rejects.toThrow("JWT must have 3 parts");
});
it("should throw an error if the access token is missing", async () => {
// Arrange
const accessToken: string = null;
const refreshToken = "refreshToken";
const vaultTimeoutAction = VaultTimeoutAction.Lock;
const vaultTimeout = 30;
// Act
const result = tokenService.setTokens(
accessToken,
vaultTimeoutAction,
vaultTimeout,
refreshToken,
);
// Assert
await expect(result).rejects.toThrow("Access token is required.");
});
it("should not throw an error if the refresh token is missing and it should just not set it", async () => {
// Arrange
const refreshToken: string = null;
const vaultTimeoutAction = VaultTimeoutAction.Lock;
const vaultTimeout = 30;
(tokenService as any).setRefreshToken = jest.fn();
// Act
await tokenService.setTokens(accessTokenJwt, vaultTimeoutAction, vaultTimeout, refreshToken);
// Assert
expect((tokenService as any).setRefreshToken).not.toHaveBeenCalled();
});
});
describe("clearTokens", () => {
it("should call to clear all tokens for the specified user id", async () => {
// Arrange
const userId = "userId" as UserId;
tokenService.clearAccessToken = jest.fn();
(tokenService as any).clearRefreshToken = jest.fn();
(tokenService as any).clearClientId = jest.fn();
(tokenService as any).clearClientSecret = jest.fn();
// Act
await tokenService.clearTokens(userId);
// Assert
expect(tokenService.clearAccessToken).toHaveBeenCalledWith(userId);
expect((tokenService as any).clearRefreshToken).toHaveBeenCalledWith(userId);
expect((tokenService as any).clearClientId).toHaveBeenCalledWith(userId);
expect((tokenService as any).clearClientSecret).toHaveBeenCalledWith(userId);
});
it("should call to clear all tokens for the active user id", async () => {
// Arrange
const userId = "userId" as UserId;
globalStateProvider.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID).stateSubject.next(userId);
tokenService.clearAccessToken = jest.fn();
(tokenService as any).clearRefreshToken = jest.fn();
(tokenService as any).clearClientId = jest.fn();
(tokenService as any).clearClientSecret = jest.fn();
// Act
await tokenService.clearTokens();
// Assert
expect(tokenService.clearAccessToken).toHaveBeenCalledWith(userId);
expect((tokenService as any).clearRefreshToken).toHaveBeenCalledWith(userId);
expect((tokenService as any).clearClientId).toHaveBeenCalledWith(userId);
expect((tokenService as any).clearClientSecret).toHaveBeenCalledWith(userId);
});
it("should not call to clear all tokens if no user id is provided and there is no active user in global state", async () => {
// Arrange
tokenService.clearAccessToken = jest.fn();
(tokenService as any).clearRefreshToken = jest.fn();
(tokenService as any).clearClientId = jest.fn();
(tokenService as any).clearClientSecret = jest.fn();
// Act
const result = tokenService.clearTokens();
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot clear tokens.");
});
});
describe("Two Factor Token methods", () => {
describe("setTwoFactorToken", () => {
it("should set the email and two factor token when there hasn't been a previous record (initializing the record)", async () => {
// Arrange
const email = "testUser@email.com";
const twoFactorToken = "twoFactorTokenForTestUser";
// Act
await tokenService.setTwoFactorToken(email, twoFactorToken);
// Assert
expect(
globalStateProvider.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL).nextMock,
).toHaveBeenCalledWith({ [email]: twoFactorToken });
});
it("should set the email and two factor token when there is an initialized value already (updating the existing record)", async () => {
// Arrange
const email = "testUser@email.com";
const twoFactorToken = "twoFactorTokenForTestUser";
const initialTwoFactorTokenRecord: Record<string, string> = {
otherUser: "otherUserTwoFactorToken",
};
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
.stateSubject.next(initialTwoFactorTokenRecord);
// Act
await tokenService.setTwoFactorToken(email, twoFactorToken);
// Assert
expect(
globalStateProvider.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL).nextMock,
).toHaveBeenCalledWith({ [email]: twoFactorToken, ...initialTwoFactorTokenRecord });
});
});
describe("getTwoFactorToken", () => {
it("should return the two factor token for the given email", async () => {
// Arrange
const email = "testUser";
const twoFactorToken = "twoFactorTokenForTestUser";
const initialTwoFactorTokenRecord: Record<string, string> = {
[email]: twoFactorToken,
};
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
.stateSubject.next(initialTwoFactorTokenRecord);
// Act
const result = await tokenService.getTwoFactorToken(email);
// Assert
expect(result).toEqual(twoFactorToken);
});
it("should not return the two factor token for an email that doesn't exist", async () => {
// Arrange
const email = "testUser";
const initialTwoFactorTokenRecord: Record<string, string> = {
otherUser: "twoFactorTokenForOtherUser",
};
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
.stateSubject.next(initialTwoFactorTokenRecord);
// Act
const result = await tokenService.getTwoFactorToken(email);
// Assert
expect(result).toEqual(undefined);
});
it("should return null if there is no two factor token record", async () => {
// Arrange
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
.stateSubject.next(null);
// Act
const result = await tokenService.getTwoFactorToken("testUser");
// Assert
expect(result).toEqual(null);
});
});
describe("clearTwoFactorToken", () => {
it("should clear the two factor token for the given email when a record exists", async () => {
// Arrange
const email = "testUser";
const twoFactorToken = "twoFactorTokenForTestUser";
const initialTwoFactorTokenRecord: Record<string, string> = {
[email]: twoFactorToken,
};
globalStateProvider
.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL)
.stateSubject.next(initialTwoFactorTokenRecord);
// Act
await tokenService.clearTwoFactorToken(email);
// Assert
expect(
globalStateProvider.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL).nextMock,
).toHaveBeenCalledWith({});
});
it("should initialize the record if it doesn't exist and delete the value", async () => {
// Arrange
const email = "testUser";
// Act
await tokenService.clearTwoFactorToken(email);
// Assert
expect(
globalStateProvider.getFake(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL).nextMock,
).toHaveBeenCalledWith({});
});
});
});
describe("Security Stamp methods", () => {
const mockSecurityStamp = "securityStamp";
describe("setSecurityStamp", () => {
it("should throw an error if no user id is provided and there is no active user in global state", async () => {
// Act
// note: don't await here because we want to test the error
const result = tokenService.setSecurityStamp(mockSecurityStamp);
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot set security stamp.");
});
it("should set the security stamp in memory when there is an active user in global state", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
await tokenService.setSecurityStamp(mockSecurityStamp);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock,
).toHaveBeenCalledWith(mockSecurityStamp);
});
it("should set the security stamp in memory for the specified user id", async () => {
// Act
await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock,
).toHaveBeenCalledWith(mockSecurityStamp);
});
});
describe("getSecurityStamp", () => {
it("should throw an error if no user id is provided and there is no active user in global state", async () => {
// Act
// note: don't await here because we want to test the error
const result = tokenService.getSecurityStamp();
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot get security stamp.");
});
it("should return the security stamp from memory with no user id specified (uses global active user)", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY)
.stateSubject.next([userIdFromAccessToken, mockSecurityStamp]);
// Act
const result = await tokenService.getSecurityStamp();
// Assert
expect(result).toEqual(mockSecurityStamp);
});
it("should return the security stamp from memory for the specified user id", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY)
.stateSubject.next([userIdFromAccessToken, mockSecurityStamp]);
// Act
const result = await tokenService.getSecurityStamp(userIdFromAccessToken);
// Assert
expect(result).toEqual(mockSecurityStamp);
});
});
});
// Helpers
function createTokenService(supportsSecureStorage: boolean) {
return new TokenService(
singleUserStateProvider,
globalStateProvider,
supportsSecureStorage,
secureStorageService,
keyGenerationService,
encryptService,
logService,
);
}
});