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; let keyGenerationService: MockProxy; let encryptService: MockProxy; let logService: MockProxy; 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(); keyGenerationService = mock(); encryptService = mock(); logService = mock(); 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 = { 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 = { [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 = { 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 = { [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, ); } });