import { TestBed } from '@/testing'; import { lastValueFrom, of, throwError } from 'rxjs'; import { vi } from 'vitest'; import { AuthStateService } from '../../auth-state/auth-state.service'; import { LoggerService } from '../../logging/logger.service'; import { StoragePersistenceService } from '../../storage/storage-persistence.service'; import { mockProvider } from '../../testing/mock'; import type { JwtKey, JwtKeys } from '../../validation/jwtkeys'; import { ValidationResult } from '../../validation/validation-result'; import type { AuthResult, CallbackContext } from '../callback-context'; import { FlowsDataService } from '../flows-data.service'; import { ResetAuthDataService } from '../reset-auth-data.service'; import { SigninKeyDataService } from '../signin-key-data.service'; import { HistoryJwtKeysCallbackHandlerService } from './history-jwt-keys-callback-handler.service'; const DUMMY_JWT_KEYS: JwtKeys = { keys: [ { kty: 'some-value1', use: 'some-value2', kid: 'some-value3', x5t: 'some-value4', e: 'some-value5', n: 'some-value6', x5c: ['some-value7'], }, ], }; describe('HistoryJwtKeysCallbackHandlerService', () => { let service: HistoryJwtKeysCallbackHandlerService; let storagePersistenceService: StoragePersistenceService; let signInKeyDataService: SigninKeyDataService; let resetAuthDataService: ResetAuthDataService; let flowsDataService: FlowsDataService; let authStateService: AuthStateService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ HistoryJwtKeysCallbackHandlerService, mockProvider(LoggerService), mockProvider(AuthStateService), mockProvider(FlowsDataService), mockProvider(SigninKeyDataService), mockProvider(StoragePersistenceService), mockProvider(ResetAuthDataService), ], }); service = TestBed.inject(HistoryJwtKeysCallbackHandlerService); storagePersistenceService = TestBed.inject(StoragePersistenceService); resetAuthDataService = TestBed.inject(ResetAuthDataService); signInKeyDataService = TestBed.inject(SigninKeyDataService); flowsDataService = TestBed.inject(FlowsDataService); authStateService = TestBed.inject(AuthStateService); }); it('should create', () => { expect(service).toBeTruthy(); }); describe('callbackHistoryAndResetJwtKeys', () => { it('writes authResult into the storage', async () => { const storagePersistenceServiceSpy = vi.spyOn( storagePersistenceService, 'write' ); const DUMMY_AUTH_RESULT = { refresh_token: 'dummy_refresh_token', id_token: 'some-id-token', }; const callbackContext = { authResult: DUMMY_AUTH_RESULT, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: true, }, ]; vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( of({ keys: [] } as JwtKeys) ); await lastValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); expect(storagePersistenceServiceSpy).toBeCalledWith([ ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['jwtKeys', { keys: [] }, allConfigs[0]], ]); expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2); }); it('writes refresh_token into the storage without reuse (refresh token rotation)', async () => { const DUMMY_AUTH_RESULT = { refresh_token: 'dummy_refresh_token', id_token: 'some-id-token', }; const storagePersistenceServiceSpy = vi.spyOn( storagePersistenceService, 'write' ); const callbackContext = { authResult: DUMMY_AUTH_RESULT, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: true, }, ]; vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( of({ keys: [] } as JwtKeys) ); await lastValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); expect(storagePersistenceServiceSpy).toBeCalledWith([ ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['jwtKeys', { keys: [] }, allConfigs[0]], ]); expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2); }); it('writes refresh_token into the storage with reuse (without refresh token rotation)', async () => { const DUMMY_AUTH_RESULT = { refresh_token: 'dummy_refresh_token', id_token: 'some-id-token', }; const storagePersistenceServiceSpy = vi.spyOn( storagePersistenceService, 'write' ); const callbackContext = { authResult: DUMMY_AUTH_RESULT, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: true, allowUnsafeReuseRefreshToken: true, }, ]; vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( of({ keys: [] } as JwtKeys) ); await lastValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); expect(storagePersistenceServiceSpy).toBeCalledWith([ ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['reusable_refresh_token', 'dummy_refresh_token', allConfigs[0]], ['jwtKeys', { keys: [] }, allConfigs[0]], ]); expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(3); }); it('resetBrowserHistory if historyCleanup is turned on and is not in a renewProcess', async () => { const DUMMY_AUTH_RESULT = { id_token: 'some-id-token', }; const callbackContext = { isRenewProcess: false, authResult: DUMMY_AUTH_RESULT, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: false, }, ]; const windowSpy = vi.spyOn(window.history, 'replaceState'); vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( of({ keys: [] } as JwtKeys) ); await lastValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); expect(windowSpy).toHaveBeenCalledTimes(1); }); it('returns callbackContext with jwtkeys filled if everything works fine', async () => { const DUMMY_AUTH_RESULT = { id_token: 'some-id-token', }; const callbackContext = { isRenewProcess: false, authResult: DUMMY_AUTH_RESULT, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: false, }, ]; vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( of({ keys: [{ kty: 'henlo' } as JwtKey] } as JwtKeys) ); const result = await lastValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); expect(result).toEqual({ isRenewProcess: false, authResult: DUMMY_AUTH_RESULT, jwtKeys: { keys: [{ kty: 'henlo' }] }, } as CallbackContext); }); it('returns error if no jwtKeys have been in the call --> keys are null', async () => { const DUMMY_AUTH_RESULT = { id_token: 'some-id-token', }; const callbackContext = { isRenewProcess: false, authResult: DUMMY_AUTH_RESULT, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: false, }, ]; vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( of({} as JwtKeys) ); try { await lastValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); } catch (err: any) { expect(err.message).toEqual( 'Failed to retrieve signing key with error: Error: Failed to retrieve signing key' ); } }); it('returns error if no jwtKeys have been in the call --> keys throw an error', async () => { const DUMMY_AUTH_RESULT = { id_token: 'some-id-token', }; const callbackContext = { isRenewProcess: false, authResult: DUMMY_AUTH_RESULT, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: false, }, ]; vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( throwError(() => new Error('error')) ); try { await lastValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); } catch (err: any) { expect(err.message).toEqual( 'Failed to retrieve signing key with error: Error: Error: error' ); } }); it('returns error if callbackContext.authresult has an error property filled', async () => { const callbackContext = { authResult: { error: 'someError' }, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: true, }, ]; try { await lastValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); } catch (err: any) { expect(err.message).toEqual( 'AuthCallback AuthResult came with error: someError' ); } }); it('calls resetAuthorizationData, resets nonce and authStateService in case of an error', async () => { const callbackContext = { authResult: { error: 'someError' }, isRenewProcess: false, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: true, }, ]; const resetAuthorizationDataSpy = vi.spyOn( resetAuthDataService, 'resetAuthorizationData' ); const setNonceSpy = vi.spyOn(flowsDataService, 'setNonce'); const updateAndPublishAuthStateSpy = vi.spyOn( authStateService, 'updateAndPublishAuthState' ); try { await lastValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); } catch { expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1); expect(setNonceSpy).toHaveBeenCalledTimes(1); expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ isAuthenticated: false, validationResult: ValidationResult.SecureTokenServerError, isRenewProcess: false, }); } }); it('calls authStateService.updateAndPublishAuthState with login required if the error is `login_required`', async () => { const callbackContext = { authResult: { error: 'login_required' }, isRenewProcess: false, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: true, }, ]; const resetAuthorizationDataSpy = vi.spyOn( resetAuthDataService, 'resetAuthorizationData' ); const setNonceSpy = vi.spyOn(flowsDataService, 'setNonce'); const updateAndPublishAuthStateSpy = vi.spyOn( authStateService, 'updateAndPublishAuthState' ); try { await lastValueFrom( service.callbackHistoryAndResetJwtKeys( callbackContext, allConfigs[0]!, allConfigs ) ); } catch { expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1); expect(setNonceSpy).toHaveBeenCalledTimes(1); expect(updateAndPublishAuthStateSpy).toHaveBeenCalledExactlyOnceWith({ isAuthenticated: false, validationResult: ValidationResult.LoginRequired, isRenewProcess: false, }); } }); it('should store jwtKeys', async () => { const DUMMY_AUTH_RESULT = { id_token: 'some-id-token', }; const initialCallbackContext = { authResult: DUMMY_AUTH_RESULT, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: true, }, ]; const storagePersistenceServiceSpy = vi.spyOn( storagePersistenceService, 'write' ); vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( of(DUMMY_JWT_KEYS) ); try { const callbackContext: CallbackContext = await lastValueFrom( service.callbackHistoryAndResetJwtKeys( initialCallbackContext, allConfigs[0]!, allConfigs ) ); expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2); expect(storagePersistenceServiceSpy).toBeCalledWith([ ['authnResult', DUMMY_AUTH_RESULT, allConfigs[0]], ['jwtKeys', DUMMY_JWT_KEYS, allConfigs[0]], ]); expect(callbackContext.jwtKeys).toEqual(DUMMY_JWT_KEYS); } catch (err: any) { expect(err).toBeFalsy(); } }); it('should not store jwtKeys on error', async () => { const authResult = { id_token: 'some-id-token', access_token: 'some-access-token', } as AuthResult; const initialCallbackContext = { authResult, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: true, }, ]; const storagePersistenceServiceSpy = vi.spyOn( storagePersistenceService, 'write' ); vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( throwError(() => new Error('Error')) ); try { const callbackContext: CallbackContext = await lastValueFrom( service.callbackHistoryAndResetJwtKeys( initialCallbackContext, allConfigs[0]!, allConfigs ) ); expect(callbackContext).toBeFalsy(); } catch (err: any) { expect(err).toBeTruthy(); expect(storagePersistenceServiceSpy).toHaveBeenCalledExactlyOnceWith( 'authnResult', authResult, allConfigs[0] ); } }); it('should fallback to stored jwtKeys on error', async () => { const authResult = { id_token: 'some-id-token', access_token: 'some-access-token', } as AuthResult; const initialCallbackContext = { authResult, } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: true, }, ]; const storagePersistenceServiceSpy = vi.spyOn( storagePersistenceService, 'read' ); storagePersistenceServiceSpy.mockReturnValue(DUMMY_JWT_KEYS); vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( throwError(() => new Error('Error')) ); try { const callbackContext: CallbackContext = await lastValueFrom( service.callbackHistoryAndResetJwtKeys( initialCallbackContext, allConfigs[0]!, allConfigs ) ); expect(storagePersistenceServiceSpy).toHaveBeenCalledExactlyOnceWith( 'jwtKeys', allConfigs[0] ); expect(callbackContext.jwtKeys).toEqual(DUMMY_JWT_KEYS); } catch (err: any) { expect(err).toBeFalsy(); } }); it('should throw error if no jwtKeys are stored', async () => { const authResult = { id_token: 'some-id-token', access_token: 'some-access-token', } as AuthResult; const initialCallbackContext = { authResult } as CallbackContext; const allConfigs = [ { configId: 'configId1', historyCleanupOff: true, }, ]; vi.spyOn(storagePersistenceService, 'read').mockReturnValue(null); vi.spyOn(signInKeyDataService, 'getSigningKeys').mockReturnValue( throwError(() => new Error('Error')) ); try { const callbackContext: CallbackContext = await lastValueFrom( service.callbackHistoryAndResetJwtKeys( initialCallbackContext, allConfigs[0]!, allConfigs ) ); expect(callbackContext).toBeFalsy(); } catch (err: any) { expect(err).toBeTruthy(); } }); }); describe('historyCleanUpTurnedOn ', () => { it('check for false if historyCleanUpTurnedOn is on', () => { const config = { configId: 'configId1', historyCleanupOff: true, }; const value = (service as any).historyCleanUpTurnedOn(config); expect(value).toEqual(false); }); it('check for true if historyCleanUpTurnedOn is off', () => { const config = { configId: 'configId1', historyCleanupOff: false, }; const value = (service as any).historyCleanUpTurnedOn(config); expect(value).toEqual(true); }); }); });