601 lines
17 KiB
TypeScript
601 lines
17 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|