2286 lines
83 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
});
|