feat: init and fork some code from angular-auth-oidc-client
Some checks are pending
Build, Lint & Test Lib / Built, Lint and Test Library (push) Waiting to run
Build, Lint & Test Lib / Angular latest (push) Blocked by required conditions
Build, Lint & Test Lib / Angular latest & Schematics Job (push) Blocked by required conditions
Build, Lint & Test Lib / Angular latest Standalone & Schematics Job (push) Blocked by required conditions
Build, Lint & Test Lib / Angular 16 & RxJs 6 (push) Blocked by required conditions
Build, Lint & Test Lib / Angular V16 (push) Blocked by required conditions
Docs / Build and Deploy Docs Job (push) Waiting to run
Docs / Close Pull Request Job (push) Waiting to run
Some checks are pending
Build, Lint & Test Lib / Built, Lint and Test Library (push) Waiting to run
Build, Lint & Test Lib / Angular latest (push) Blocked by required conditions
Build, Lint & Test Lib / Angular latest & Schematics Job (push) Blocked by required conditions
Build, Lint & Test Lib / Angular latest Standalone & Schematics Job (push) Blocked by required conditions
Build, Lint & Test Lib / Angular 16 & RxJs 6 (push) Blocked by required conditions
Build, Lint & Test Lib / Angular V16 (push) Blocked by required conditions
Docs / Build and Deploy Docs Job (push) Waiting to run
Docs / Close Pull Request Job (push) Waiting to run
This commit is contained in:
30
src/flows/callback-context.ts
Normal file
30
src/flows/callback-context.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { JwtKeys } from '../validation/jwtkeys';
|
||||
import { StateValidationResult } from '../validation/state-validation-result';
|
||||
|
||||
export interface CallbackContext {
|
||||
code: string;
|
||||
refreshToken: string;
|
||||
state: string;
|
||||
sessionState: string | null;
|
||||
authResult: AuthResult | null;
|
||||
isRenewProcess: boolean;
|
||||
jwtKeys: JwtKeys | null;
|
||||
validationResult: StateValidationResult | null;
|
||||
existingIdToken: string | null;
|
||||
}
|
||||
|
||||
export interface AuthResult {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
id_token?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
access_token?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
refresh_token?: string;
|
||||
error?: any;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
session_state?: any;
|
||||
state?: any;
|
||||
scope?: string;
|
||||
expires_in?: number;
|
||||
token_type?: string;
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
import { HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { mockProvider } from '../../../test/auto-mock';
|
||||
import { createRetriableStream } from '../../../test/create-retriable-stream.helper';
|
||||
import { DataService } from '../../api/data.service';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { StoragePersistenceService } from '../../storage/storage-persistence.service';
|
||||
import { UrlService } from '../../utils/url/url.service';
|
||||
import { TokenValidationService } from '../../validation/token-validation.service';
|
||||
import { CallbackContext } from '../callback-context';
|
||||
import { FlowsDataService } from '../flows-data.service';
|
||||
import { CodeFlowCallbackHandlerService } from './code-flow-callback-handler.service';
|
||||
|
||||
describe('CodeFlowCallbackHandlerService', () => {
|
||||
let service: CodeFlowCallbackHandlerService;
|
||||
let dataService: DataService;
|
||||
let storagePersistenceService: StoragePersistenceService;
|
||||
let tokenValidationService: TokenValidationService;
|
||||
let urlService: UrlService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
CodeFlowCallbackHandlerService,
|
||||
mockProvider(UrlService),
|
||||
mockProvider(LoggerService),
|
||||
mockProvider(TokenValidationService),
|
||||
mockProvider(FlowsDataService),
|
||||
mockProvider(StoragePersistenceService),
|
||||
mockProvider(DataService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(CodeFlowCallbackHandlerService);
|
||||
dataService = TestBed.inject(DataService);
|
||||
urlService = TestBed.inject(UrlService);
|
||||
storagePersistenceService = TestBed.inject(StoragePersistenceService);
|
||||
tokenValidationService = TestBed.inject(TokenValidationService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('codeFlowCallback', () => {
|
||||
it('throws error if no state is given', waitForAsync(() => {
|
||||
const getUrlParameterSpy = spyOn(
|
||||
urlService,
|
||||
'getUrlParameter'
|
||||
).and.returnValue('params');
|
||||
|
||||
getUrlParameterSpy.withArgs('test-url', 'state').and.returnValue('');
|
||||
|
||||
service
|
||||
.codeFlowCallback('test-url', { configId: 'configId1' })
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('throws error if no code is given', waitForAsync(() => {
|
||||
const getUrlParameterSpy = spyOn(
|
||||
urlService,
|
||||
'getUrlParameter'
|
||||
).and.returnValue('params');
|
||||
|
||||
getUrlParameterSpy.withArgs('test-url', 'code').and.returnValue('');
|
||||
|
||||
service
|
||||
.codeFlowCallback('test-url', { configId: 'configId1' })
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('returns callbackContext if all params are good', waitForAsync(() => {
|
||||
spyOn(urlService, 'getUrlParameter').and.returnValue('params');
|
||||
|
||||
const expectedCallbackContext = {
|
||||
code: 'params',
|
||||
refreshToken: '',
|
||||
state: 'params',
|
||||
sessionState: 'params',
|
||||
authResult: null,
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: null,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
|
||||
service
|
||||
.codeFlowCallback('test-url', { configId: 'configId1' })
|
||||
.subscribe((callbackContext) => {
|
||||
expect(callbackContext).toEqual(expectedCallbackContext);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('codeFlowCodeRequest ', () => {
|
||||
const HTTP_ERROR = new HttpErrorResponse({});
|
||||
const CONNECTION_ERROR = new HttpErrorResponse({
|
||||
error: new ProgressEvent('error'),
|
||||
status: 0,
|
||||
statusText: 'Unknown Error',
|
||||
url: 'https://identity-server.test/openid-connect/token',
|
||||
});
|
||||
|
||||
it('throws error if state is not correct', waitForAsync(() => {
|
||||
spyOn(
|
||||
tokenValidationService,
|
||||
'validateStateFromHashCallback'
|
||||
).and.returnValue(false);
|
||||
|
||||
service
|
||||
.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1' })
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('throws error if authWellknownEndpoints is null is given', waitForAsync(() => {
|
||||
spyOn(
|
||||
tokenValidationService,
|
||||
'validateStateFromHashCallback'
|
||||
).and.returnValue(true);
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', { configId: 'configId1' })
|
||||
.and.returnValue(null);
|
||||
|
||||
service
|
||||
.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1' })
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('throws error if tokenendpoint is null is given', waitForAsync(() => {
|
||||
spyOn(
|
||||
tokenValidationService,
|
||||
'validateStateFromHashCallback'
|
||||
).and.returnValue(true);
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', { configId: 'configId1' })
|
||||
.and.returnValue({ tokenEndpoint: null });
|
||||
|
||||
service
|
||||
.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1' })
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls dataService if all params are good', waitForAsync(() => {
|
||||
const postSpy = spyOn(dataService, 'post').and.returnValue(of({}));
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', { configId: 'configId1' })
|
||||
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
|
||||
|
||||
spyOn(
|
||||
tokenValidationService,
|
||||
'validateStateFromHashCallback'
|
||||
).and.returnValue(true);
|
||||
|
||||
service
|
||||
.codeFlowCodeRequest({} as CallbackContext, { configId: 'configId1' })
|
||||
.subscribe(() => {
|
||||
expect(postSpy).toHaveBeenCalledOnceWith(
|
||||
'tokenEndpoint',
|
||||
undefined,
|
||||
{ configId: 'configId1' },
|
||||
jasmine.any(HttpHeaders)
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls url service with custom token params', waitForAsync(() => {
|
||||
const urlServiceSpy = spyOn(
|
||||
urlService,
|
||||
'createBodyForCodeFlowCodeRequest'
|
||||
);
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
customParamsCodeRequest: { foo: 'bar' },
|
||||
};
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', config)
|
||||
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
|
||||
|
||||
spyOn(
|
||||
tokenValidationService,
|
||||
'validateStateFromHashCallback'
|
||||
).and.returnValue(true);
|
||||
|
||||
const postSpy = spyOn(dataService, 'post').and.returnValue(of({}));
|
||||
|
||||
service
|
||||
.codeFlowCodeRequest({ code: 'foo' } as CallbackContext, config)
|
||||
.subscribe(() => {
|
||||
expect(urlServiceSpy).toHaveBeenCalledOnceWith('foo', config, {
|
||||
foo: 'bar',
|
||||
});
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls dataService with correct headers if all params are good', waitForAsync(() => {
|
||||
const postSpy = spyOn(dataService, 'post').and.returnValue(of({}));
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
customParamsCodeRequest: { foo: 'bar' },
|
||||
};
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', config)
|
||||
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
|
||||
|
||||
spyOn(
|
||||
tokenValidationService,
|
||||
'validateStateFromHashCallback'
|
||||
).and.returnValue(true);
|
||||
|
||||
service
|
||||
.codeFlowCodeRequest({} as CallbackContext, config)
|
||||
.subscribe(() => {
|
||||
const httpHeaders = postSpy.calls.mostRecent().args[3] as HttpHeaders;
|
||||
|
||||
expect(httpHeaders.has('Content-Type')).toBeTrue();
|
||||
expect(httpHeaders.get('Content-Type')).toBe(
|
||||
'application/x-www-form-urlencoded'
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
it('returns error in case of http error', waitForAsync(() => {
|
||||
spyOn(dataService, 'post').and.returnValue(throwError(() => HTTP_ERROR));
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
customParamsCodeRequest: { foo: 'bar' },
|
||||
authority: 'authority',
|
||||
};
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', config)
|
||||
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
|
||||
|
||||
service.codeFlowCodeRequest({} as CallbackContext, config).subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('retries request in case of no connection http error and succeeds', waitForAsync(() => {
|
||||
const postSpy = spyOn(dataService, 'post').and.returnValue(
|
||||
createRetriableStream(
|
||||
throwError(() => CONNECTION_ERROR),
|
||||
of({})
|
||||
)
|
||||
);
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
customParamsCodeRequest: { foo: 'bar' },
|
||||
authority: 'authority',
|
||||
};
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', config)
|
||||
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
|
||||
|
||||
spyOn(
|
||||
tokenValidationService,
|
||||
'validateStateFromHashCallback'
|
||||
).and.returnValue(true);
|
||||
|
||||
service.codeFlowCodeRequest({} as CallbackContext, config).subscribe({
|
||||
next: (res) => {
|
||||
expect(res).toBeTruthy();
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
error: (err) => {
|
||||
// fails if there should be a result
|
||||
expect(err).toBeFalsy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('retries request in case of no connection http error and fails because of http error afterwards', waitForAsync(() => {
|
||||
const postSpy = spyOn(dataService, 'post').and.returnValue(
|
||||
createRetriableStream(
|
||||
throwError(() => CONNECTION_ERROR),
|
||||
throwError(() => HTTP_ERROR)
|
||||
)
|
||||
);
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
customParamsCodeRequest: { foo: 'bar' },
|
||||
authority: 'authority',
|
||||
};
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', config)
|
||||
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
|
||||
|
||||
spyOn(
|
||||
tokenValidationService,
|
||||
'validateStateFromHashCallback'
|
||||
).and.returnValue(true);
|
||||
|
||||
service.codeFlowCodeRequest({} as CallbackContext, config).subscribe({
|
||||
next: (res) => {
|
||||
// fails if there should be a result
|
||||
expect(res).toBeFalsy();
|
||||
},
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { HttpHeaders } from '@ngify/http';
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Observable, of, throwError, timer } from 'rxjs';
|
||||
import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators';
|
||||
import { DataService } from '../../api/data.service';
|
||||
import { OpenIdConfiguration } from '../../config/openid-configuration';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { StoragePersistenceService } from '../../storage/storage-persistence.service';
|
||||
import { UrlService } from '../../utils/url/url.service';
|
||||
import { TokenValidationService } from '../../validation/token-validation.service';
|
||||
import { AuthResult, CallbackContext } from '../callback-context';
|
||||
import { FlowsDataService } from '../flows-data.service';
|
||||
import { isNetworkError } from './error-helper';
|
||||
|
||||
@Injectable()
|
||||
export class CodeFlowCallbackHandlerService {
|
||||
private readonly urlService = inject(UrlService);
|
||||
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly tokenValidationService = inject(TokenValidationService);
|
||||
|
||||
private readonly flowsDataService = inject(FlowsDataService);
|
||||
|
||||
private readonly storagePersistenceService = inject(
|
||||
StoragePersistenceService
|
||||
);
|
||||
|
||||
private readonly dataService = inject(DataService);
|
||||
|
||||
// STEP 1 Code Flow
|
||||
codeFlowCallback(
|
||||
urlToCheck: string,
|
||||
config: OpenIdConfiguration
|
||||
): Observable<CallbackContext> {
|
||||
const code = this.urlService.getUrlParameter(urlToCheck, 'code');
|
||||
const state = this.urlService.getUrlParameter(urlToCheck, 'state');
|
||||
const sessionState = this.urlService.getUrlParameter(
|
||||
urlToCheck,
|
||||
'session_state'
|
||||
);
|
||||
|
||||
if (!state) {
|
||||
this.loggerService.logDebug(config, 'no state in url');
|
||||
|
||||
return throwError(() => new Error('no state in url'));
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
this.loggerService.logDebug(config, 'no code in url');
|
||||
|
||||
return throwError(() => new Error('no code in url'));
|
||||
}
|
||||
|
||||
this.loggerService.logDebug(
|
||||
config,
|
||||
'running validation for callback',
|
||||
urlToCheck
|
||||
);
|
||||
|
||||
const initialCallbackContext: CallbackContext = {
|
||||
code,
|
||||
refreshToken: '',
|
||||
state,
|
||||
sessionState,
|
||||
authResult: null,
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: null,
|
||||
existingIdToken: null,
|
||||
};
|
||||
|
||||
return of(initialCallbackContext);
|
||||
}
|
||||
|
||||
// STEP 2 Code Flow // Code Flow Silent Renew starts here
|
||||
codeFlowCodeRequest(
|
||||
callbackContext: CallbackContext,
|
||||
config: OpenIdConfiguration
|
||||
): Observable<CallbackContext> {
|
||||
const authStateControl = this.flowsDataService.getAuthStateControl(config);
|
||||
const isStateCorrect =
|
||||
this.tokenValidationService.validateStateFromHashCallback(
|
||||
callbackContext.state,
|
||||
authStateControl,
|
||||
config
|
||||
);
|
||||
|
||||
if (!isStateCorrect) {
|
||||
return throwError(() => new Error('codeFlowCodeRequest incorrect state'));
|
||||
}
|
||||
|
||||
const authWellknownEndpoints = this.storagePersistenceService.read(
|
||||
'authWellKnownEndPoints',
|
||||
config
|
||||
);
|
||||
const tokenEndpoint = authWellknownEndpoints?.tokenEndpoint;
|
||||
|
||||
if (!tokenEndpoint) {
|
||||
return throwError(() => new Error('Token Endpoint not defined'));
|
||||
}
|
||||
|
||||
let headers: HttpHeaders = new HttpHeaders();
|
||||
|
||||
headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
||||
|
||||
const bodyForCodeFlow = this.urlService.createBodyForCodeFlowCodeRequest(
|
||||
callbackContext.code,
|
||||
config,
|
||||
config?.customParamsCodeRequest
|
||||
);
|
||||
|
||||
return this.dataService
|
||||
.post(tokenEndpoint, bodyForCodeFlow, config, headers)
|
||||
.pipe(
|
||||
switchMap((response) => {
|
||||
if (response) {
|
||||
const authResult: AuthResult = {
|
||||
...response,
|
||||
state: callbackContext.state,
|
||||
session_state: callbackContext.sessionState,
|
||||
};
|
||||
|
||||
callbackContext.authResult = authResult;
|
||||
}
|
||||
|
||||
return of(callbackContext);
|
||||
}),
|
||||
retryWhen((error) => this.handleRefreshRetry(error, config)),
|
||||
catchError((error) => {
|
||||
const { authority } = config;
|
||||
const errorMessage = `OidcService code request ${authority}`;
|
||||
|
||||
this.loggerService.logError(config, errorMessage, error);
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleRefreshRetry(
|
||||
errors: Observable<unknown>,
|
||||
config: OpenIdConfiguration
|
||||
): Observable<unknown> {
|
||||
return errors.pipe(
|
||||
mergeMap((error) => {
|
||||
// retry token refresh if there is no internet connection
|
||||
if (isNetworkError(error)) {
|
||||
const { authority, refreshTokenRetryInSeconds } = config;
|
||||
const errorMessage = `OidcService code request ${authority} - no internet connection`;
|
||||
|
||||
this.loggerService.logWarning(config, errorMessage, error);
|
||||
|
||||
return timer((refreshTokenRetryInSeconds ?? 0) * 1000);
|
||||
}
|
||||
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
57
src/flows/callback-handling/error-helper.spec.ts
Normal file
57
src/flows/callback-handling/error-helper.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { isNetworkError } from './error-helper';
|
||||
|
||||
describe('error helper', () => {
|
||||
describe('isNetworkError ', () => {
|
||||
const HTTP_ERROR = new HttpErrorResponse({});
|
||||
|
||||
const CONNECTION_ERROR = new HttpErrorResponse({
|
||||
error: new ProgressEvent('error'),
|
||||
status: 0,
|
||||
statusText: 'Unknown Error',
|
||||
url: 'https://identity-server.test/openid-connect/token',
|
||||
});
|
||||
|
||||
const UNKNOWN_CONNECTION_ERROR = new HttpErrorResponse({
|
||||
error: new Error('error'),
|
||||
status: 0,
|
||||
statusText: 'Unknown Error',
|
||||
url: 'https://identity-server.test/openid-connect/token',
|
||||
});
|
||||
|
||||
const PARTIAL_CONNECTION_ERROR = new HttpErrorResponse({
|
||||
error: new ProgressEvent('error'),
|
||||
status: 418, // i am a teapot
|
||||
statusText: 'Unknown Error',
|
||||
url: 'https://identity-server.test/openid-connect/token',
|
||||
});
|
||||
|
||||
it('returns true on http error with status = 0', () => {
|
||||
expect(isNetworkError(CONNECTION_ERROR)).toBeTrue();
|
||||
});
|
||||
|
||||
it('returns true on http error with status = 0 and unknown error', () => {
|
||||
expect(isNetworkError(UNKNOWN_CONNECTION_ERROR)).toBeTrue();
|
||||
});
|
||||
|
||||
it('returns true on http error with status <> 0 and error ProgressEvent', () => {
|
||||
expect(isNetworkError(PARTIAL_CONNECTION_ERROR)).toBeTrue();
|
||||
});
|
||||
|
||||
it('returns false on non http error', () => {
|
||||
expect(isNetworkError(new Error('not a HttpErrorResponse'))).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns false on string error', () => {
|
||||
expect(isNetworkError('not a HttpErrorResponse')).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns false on undefined', () => {
|
||||
expect(isNetworkError(undefined)).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns false on empty http error', () => {
|
||||
expect(isNetworkError(HTTP_ERROR)).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
14
src/flows/callback-handling/error-helper.ts
Normal file
14
src/flows/callback-handling/error-helper.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { HttpErrorResponse } from '@ngify/http';
|
||||
|
||||
/**
|
||||
* checks if the error is a network error
|
||||
* by checking if either internal error is a ProgressEvent with type error
|
||||
* or another error with status 0
|
||||
* @param error
|
||||
* @returns true if the error is a network error
|
||||
*/
|
||||
export const isNetworkError = (error: unknown): boolean =>
|
||||
!!error &&
|
||||
error instanceof HttpErrorResponse &&
|
||||
((error.error instanceof ProgressEvent && error.error.type === 'error') ||
|
||||
(error.status === 0 && !!error.error));
|
||||
@@ -0,0 +1,621 @@
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { mockProvider } from '../../../test/auto-mock';
|
||||
import { AuthStateService } from '../../auth-state/auth-state.service';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { StoragePersistenceService } from '../../storage/storage-persistence.service';
|
||||
import { JwtKey, JwtKeys } from '../../validation/jwtkeys';
|
||||
import { ValidationResult } from '../../validation/validation-result';
|
||||
import { 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),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
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', waitForAsync(() => {
|
||||
const storagePersistenceServiceSpy = 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,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
|
||||
of({ keys: [] } as JwtKeys)
|
||||
);
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe(() => {
|
||||
expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
|
||||
['authnResult', DUMMY_AUTH_RESULT, allconfigs[0]],
|
||||
['jwtKeys', { keys: [] }, allconfigs[0]],
|
||||
]);
|
||||
// write authnResult & jwtKeys
|
||||
expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
}));
|
||||
|
||||
it('writes refresh_token into the storage without reuse (refresh token rotation)', waitForAsync(() => {
|
||||
const DUMMY_AUTH_RESULT = {
|
||||
refresh_token: 'dummy_refresh_token',
|
||||
id_token: 'some-id-token',
|
||||
};
|
||||
|
||||
const storagePersistenceServiceSpy = spyOn(
|
||||
storagePersistenceService,
|
||||
'write'
|
||||
);
|
||||
const callbackContext = {
|
||||
authResult: DUMMY_AUTH_RESULT,
|
||||
} as CallbackContext;
|
||||
const allconfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
historyCleanupOff: true,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
|
||||
of({ keys: [] } as JwtKeys)
|
||||
);
|
||||
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe(() => {
|
||||
expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
|
||||
['authnResult', DUMMY_AUTH_RESULT, allconfigs[0]],
|
||||
['jwtKeys', { keys: [] }, allconfigs[0]],
|
||||
]);
|
||||
// write authnResult & refresh_token & jwtKeys
|
||||
expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
}));
|
||||
|
||||
it('writes refresh_token into the storage with reuse (without refresh token rotation)', waitForAsync(() => {
|
||||
const DUMMY_AUTH_RESULT = {
|
||||
refresh_token: 'dummy_refresh_token',
|
||||
id_token: 'some-id-token',
|
||||
};
|
||||
|
||||
const storagePersistenceServiceSpy = spyOn(
|
||||
storagePersistenceService,
|
||||
'write'
|
||||
);
|
||||
const callbackContext = {
|
||||
authResult: DUMMY_AUTH_RESULT,
|
||||
} as CallbackContext;
|
||||
const allconfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
historyCleanupOff: true,
|
||||
allowUnsafeReuseRefreshToken: true,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
|
||||
of({ keys: [] } as JwtKeys)
|
||||
);
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe(() => {
|
||||
expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
|
||||
['authnResult', DUMMY_AUTH_RESULT, allconfigs[0]],
|
||||
['reusable_refresh_token', 'dummy_refresh_token', allconfigs[0]],
|
||||
['jwtKeys', { keys: [] }, allconfigs[0]],
|
||||
]);
|
||||
// write authnResult & refresh_token & jwtKeys
|
||||
expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
}));
|
||||
|
||||
it('resetBrowserHistory if historyCleanup is turned on and is not in a renewProcess', waitForAsync(() => {
|
||||
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 = spyOn(window.history, 'replaceState');
|
||||
|
||||
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
|
||||
of({ keys: [] } as JwtKeys)
|
||||
);
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe(() => {
|
||||
expect(windowSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
}));
|
||||
|
||||
it('returns callbackContext with jwtkeys filled if everything works fine', waitForAsync(() => {
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
|
||||
of({ keys: [{ kty: 'henlo' } as JwtKey] } as JwtKeys)
|
||||
);
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe((result) => {
|
||||
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', waitForAsync(() => {
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
|
||||
of({} as JwtKeys)
|
||||
);
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
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', waitForAsync(() => {
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
|
||||
throwError(() => new Error('error'))
|
||||
);
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
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', waitForAsync(() => {
|
||||
const callbackContext = {
|
||||
authResult: { error: 'someError' },
|
||||
} as CallbackContext;
|
||||
const allconfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
historyCleanupOff: true,
|
||||
},
|
||||
];
|
||||
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(err.message).toEqual(
|
||||
`AuthCallback AuthResult came with error: someError`
|
||||
);
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls resetAuthorizationData, resets nonce and authStateService in case of an error', waitForAsync(() => {
|
||||
const callbackContext = {
|
||||
authResult: { error: 'someError' },
|
||||
isRenewProcess: false,
|
||||
} as CallbackContext;
|
||||
const allconfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
historyCleanupOff: true,
|
||||
},
|
||||
];
|
||||
|
||||
const resetAuthorizationDataSpy = spyOn(
|
||||
resetAuthDataService,
|
||||
'resetAuthorizationData'
|
||||
);
|
||||
const setNonceSpy = spyOn(flowsDataService, 'setNonce');
|
||||
const updateAndPublishAuthStateSpy = spyOn(
|
||||
authStateService,
|
||||
'updateAndPublishAuthState'
|
||||
);
|
||||
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe({
|
||||
error: () => {
|
||||
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
|
||||
expect(setNonceSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
|
||||
isAuthenticated: false,
|
||||
validationResult: ValidationResult.SecureTokenServerError,
|
||||
isRenewProcess: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls authStateService.updateAndPublishAuthState with login required if the error is `login_required`', waitForAsync(() => {
|
||||
const callbackContext = {
|
||||
authResult: { error: 'login_required' },
|
||||
isRenewProcess: false,
|
||||
} as CallbackContext;
|
||||
const allconfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
historyCleanupOff: true,
|
||||
},
|
||||
];
|
||||
|
||||
const resetAuthorizationDataSpy = spyOn(
|
||||
resetAuthDataService,
|
||||
'resetAuthorizationData'
|
||||
);
|
||||
const setNonceSpy = spyOn(flowsDataService, 'setNonce');
|
||||
const updateAndPublishAuthStateSpy = spyOn(
|
||||
authStateService,
|
||||
'updateAndPublishAuthState'
|
||||
);
|
||||
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe({
|
||||
error: () => {
|
||||
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
|
||||
expect(setNonceSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
|
||||
isAuthenticated: false,
|
||||
validationResult: ValidationResult.LoginRequired,
|
||||
isRenewProcess: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('should store jwtKeys', waitForAsync(() => {
|
||||
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 = spyOn(
|
||||
storagePersistenceService,
|
||||
'write'
|
||||
);
|
||||
|
||||
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
|
||||
of(DUMMY_JWT_KEYS)
|
||||
);
|
||||
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
initialCallbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe({
|
||||
next: (callbackContext: CallbackContext) => {
|
||||
expect(storagePersistenceServiceSpy).toHaveBeenCalledTimes(2);
|
||||
expect(storagePersistenceServiceSpy.calls.allArgs()).toEqual([
|
||||
['authnResult', DUMMY_AUTH_RESULT, allconfigs[0]],
|
||||
['jwtKeys', DUMMY_JWT_KEYS, allconfigs[0]],
|
||||
]);
|
||||
|
||||
expect(callbackContext.jwtKeys).toEqual(DUMMY_JWT_KEYS);
|
||||
},
|
||||
error: (err) => {
|
||||
expect(err).toBeFalsy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not store jwtKeys on error', waitForAsync(() => {
|
||||
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 = spyOn(
|
||||
storagePersistenceService,
|
||||
'write'
|
||||
);
|
||||
|
||||
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
|
||||
throwError(() => new Error('Error'))
|
||||
);
|
||||
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
initialCallbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe({
|
||||
next: (callbackContext: CallbackContext) => {
|
||||
expect(callbackContext).toBeFalsy();
|
||||
},
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
|
||||
// storagePersistenceService.write() should not have been called with jwtKeys
|
||||
expect(storagePersistenceServiceSpy).toHaveBeenCalledOnceWith(
|
||||
'authnResult',
|
||||
authResult,
|
||||
allconfigs[0]
|
||||
);
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('should fallback to stored jwtKeys on error', waitForAsync(() => {
|
||||
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 = spyOn(
|
||||
storagePersistenceService,
|
||||
'read'
|
||||
);
|
||||
|
||||
storagePersistenceServiceSpy.and.returnValue(DUMMY_JWT_KEYS);
|
||||
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
|
||||
throwError(() => new Error('Error'))
|
||||
);
|
||||
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
initialCallbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe({
|
||||
next: (callbackContext: CallbackContext) => {
|
||||
expect(storagePersistenceServiceSpy).toHaveBeenCalledOnceWith(
|
||||
'jwtKeys',
|
||||
allconfigs[0]
|
||||
);
|
||||
expect(callbackContext.jwtKeys).toEqual(DUMMY_JWT_KEYS);
|
||||
},
|
||||
error: (err) => {
|
||||
expect(err).toBeFalsy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('should throw error if no jwtKeys are stored', waitForAsync(() => {
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(storagePersistenceService, 'read').and.returnValue(null);
|
||||
spyOn(signInKeyDataService, 'getSigningKeys').and.returnValue(
|
||||
throwError(() => new Error('Error'))
|
||||
);
|
||||
|
||||
service
|
||||
.callbackHistoryAndResetJwtKeys(
|
||||
initialCallbackContext,
|
||||
allconfigs[0],
|
||||
allconfigs
|
||||
)
|
||||
.subscribe({
|
||||
next: (callbackContext: CallbackContext) => {
|
||||
expect(callbackContext).toBeFalsy();
|
||||
},
|
||||
error: (err) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
import { DOCUMENT } from '../../dom';
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Observable, of, throwError } from 'rxjs';
|
||||
import { catchError, switchMap, tap } from 'rxjs/operators';
|
||||
import { AuthStateService } from '../../auth-state/auth-state.service';
|
||||
import { OpenIdConfiguration } from '../../config/openid-configuration';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { StoragePersistenceService } from '../../storage/storage-persistence.service';
|
||||
import { JwtKeys } from '../../validation/jwtkeys';
|
||||
import { ValidationResult } from '../../validation/validation-result';
|
||||
import { CallbackContext } from '../callback-context';
|
||||
import { FlowsDataService } from '../flows-data.service';
|
||||
import { ResetAuthDataService } from '../reset-auth-data.service';
|
||||
import { SigninKeyDataService } from '../signin-key-data.service';
|
||||
|
||||
const JWT_KEYS = 'jwtKeys';
|
||||
|
||||
@Injectable()
|
||||
export class HistoryJwtKeysCallbackHandlerService {
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly authStateService = inject(AuthStateService);
|
||||
|
||||
private readonly flowsDataService = inject(FlowsDataService);
|
||||
|
||||
private readonly signInKeyDataService = inject(SigninKeyDataService);
|
||||
|
||||
private readonly storagePersistenceService = inject(
|
||||
StoragePersistenceService
|
||||
);
|
||||
|
||||
private readonly resetAuthDataService = inject(ResetAuthDataService);
|
||||
|
||||
private readonly document = inject(DOCUMENT);
|
||||
|
||||
// STEP 3 Code Flow, STEP 2 Implicit Flow, STEP 3 Refresh Token
|
||||
callbackHistoryAndResetJwtKeys(
|
||||
callbackContext: CallbackContext,
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[]
|
||||
): Observable<CallbackContext> {
|
||||
let toWrite = { ...callbackContext.authResult };
|
||||
|
||||
if (!this.responseHasIdToken(callbackContext)) {
|
||||
const existingIdToken = this.storagePersistenceService.getIdToken(config);
|
||||
|
||||
toWrite = {
|
||||
...toWrite,
|
||||
id_token: existingIdToken,
|
||||
};
|
||||
}
|
||||
|
||||
this.storagePersistenceService.write('authnResult', toWrite, config);
|
||||
|
||||
if (
|
||||
config.allowUnsafeReuseRefreshToken &&
|
||||
callbackContext.authResult?.refresh_token
|
||||
) {
|
||||
this.storagePersistenceService.write(
|
||||
'reusable_refresh_token',
|
||||
callbackContext.authResult.refresh_token,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.historyCleanUpTurnedOn(config) &&
|
||||
!callbackContext.isRenewProcess
|
||||
) {
|
||||
this.resetBrowserHistory();
|
||||
} else {
|
||||
this.loggerService.logDebug(config, 'history clean up inactive');
|
||||
}
|
||||
|
||||
if (callbackContext.authResult?.error) {
|
||||
const errorMessage = `AuthCallback AuthResult came with error: ${callbackContext.authResult.error}`;
|
||||
|
||||
this.loggerService.logDebug(config, errorMessage);
|
||||
this.resetAuthDataService.resetAuthorizationData(config, allConfigs);
|
||||
this.flowsDataService.setNonce('', config);
|
||||
this.handleResultErrorFromCallback(
|
||||
callbackContext.authResult,
|
||||
callbackContext.isRenewProcess
|
||||
);
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
}
|
||||
|
||||
this.loggerService.logDebug(
|
||||
config,
|
||||
`AuthResult '${JSON.stringify(callbackContext.authResult, null, 2)}'.
|
||||
AuthCallback created, begin token validation`
|
||||
);
|
||||
|
||||
return this.signInKeyDataService.getSigningKeys(config).pipe(
|
||||
tap((jwtKeys: JwtKeys) => this.storeSigningKeys(jwtKeys, config)),
|
||||
catchError((err) => {
|
||||
// fallback: try to load jwtKeys from storage
|
||||
const storedJwtKeys = this.readSigningKeys(config);
|
||||
|
||||
if (!!storedJwtKeys) {
|
||||
this.loggerService.logWarning(
|
||||
config,
|
||||
`Failed to retrieve signing keys, fallback to stored keys`
|
||||
);
|
||||
|
||||
return of(storedJwtKeys);
|
||||
}
|
||||
|
||||
return throwError(() => new Error(err));
|
||||
}),
|
||||
switchMap((jwtKeys) => {
|
||||
if (jwtKeys) {
|
||||
callbackContext.jwtKeys = jwtKeys;
|
||||
|
||||
return of(callbackContext);
|
||||
}
|
||||
|
||||
const errorMessage = `Failed to retrieve signing key`;
|
||||
|
||||
this.loggerService.logWarning(config, errorMessage);
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
}),
|
||||
catchError((err) => {
|
||||
const errorMessage = `Failed to retrieve signing key with error: ${err}`;
|
||||
|
||||
this.loggerService.logWarning(config, errorMessage);
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private responseHasIdToken(callbackContext: CallbackContext): boolean {
|
||||
return !!callbackContext?.authResult?.id_token;
|
||||
}
|
||||
|
||||
private handleResultErrorFromCallback(
|
||||
result: unknown,
|
||||
isRenewProcess: boolean
|
||||
): void {
|
||||
let validationResult = ValidationResult.SecureTokenServerError;
|
||||
|
||||
if (
|
||||
result &&
|
||||
typeof result === 'object' &&
|
||||
'error' in result &&
|
||||
(result.error as string) === 'login_required'
|
||||
) {
|
||||
validationResult = ValidationResult.LoginRequired;
|
||||
}
|
||||
|
||||
this.authStateService.updateAndPublishAuthState({
|
||||
isAuthenticated: false,
|
||||
validationResult,
|
||||
isRenewProcess,
|
||||
});
|
||||
}
|
||||
|
||||
private historyCleanUpTurnedOn(config: OpenIdConfiguration): boolean {
|
||||
const { historyCleanupOff } = config;
|
||||
|
||||
return !historyCleanupOff;
|
||||
}
|
||||
|
||||
private resetBrowserHistory(): void {
|
||||
this.document.defaultView?.history.replaceState(
|
||||
{},
|
||||
this.document.title,
|
||||
this.document.defaultView.location.origin +
|
||||
this.document.defaultView.location.pathname
|
||||
);
|
||||
}
|
||||
|
||||
private storeSigningKeys(
|
||||
jwtKeys: JwtKeys,
|
||||
config: OpenIdConfiguration
|
||||
): void {
|
||||
this.storagePersistenceService.write(JWT_KEYS, jwtKeys, config);
|
||||
}
|
||||
|
||||
private readSigningKeys(config: OpenIdConfiguration): any {
|
||||
return this.storagePersistenceService.read(JWT_KEYS, config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { DOCUMENT } from '../../dom';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { mockProvider } from '../../../test/auto-mock';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { CallbackContext } from '../callback-context';
|
||||
import { FlowsDataService } from '../flows-data.service';
|
||||
import { ResetAuthDataService } from '../reset-auth-data.service';
|
||||
import { ImplicitFlowCallbackHandlerService } from './implicit-flow-callback-handler.service';
|
||||
|
||||
describe('ImplicitFlowCallbackHandlerService', () => {
|
||||
let service: ImplicitFlowCallbackHandlerService;
|
||||
let flowsDataService: FlowsDataService;
|
||||
let resetAuthDataService: ResetAuthDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ImplicitFlowCallbackHandlerService,
|
||||
mockProvider(FlowsDataService),
|
||||
mockProvider(ResetAuthDataService),
|
||||
mockProvider(LoggerService),
|
||||
{
|
||||
provide: DOCUMENT,
|
||||
useValue: {
|
||||
location: {
|
||||
get hash(): string {
|
||||
return '&anyFakeHash';
|
||||
},
|
||||
set hash(_value) {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(ImplicitFlowCallbackHandlerService);
|
||||
flowsDataService = TestBed.inject(FlowsDataService);
|
||||
resetAuthDataService = TestBed.inject(ResetAuthDataService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('implicitFlowCallback', () => {
|
||||
it('calls "resetAuthorizationData" if silent renew is not running', waitForAsync(() => {
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
|
||||
const resetAuthorizationDataSpy = spyOn(
|
||||
resetAuthDataService,
|
||||
'resetAuthorizationData'
|
||||
);
|
||||
const allconfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
},
|
||||
];
|
||||
|
||||
service
|
||||
.implicitFlowCallback(allconfigs[0], allconfigs, 'any-hash')
|
||||
.subscribe(() => {
|
||||
expect(resetAuthorizationDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('does NOT calls "resetAuthorizationData" if silent renew is running', waitForAsync(() => {
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
|
||||
const resetAuthorizationDataSpy = spyOn(
|
||||
resetAuthDataService,
|
||||
'resetAuthorizationData'
|
||||
);
|
||||
const allconfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
},
|
||||
];
|
||||
|
||||
service
|
||||
.implicitFlowCallback(allconfigs[0], allconfigs, 'any-hash')
|
||||
.subscribe(() => {
|
||||
expect(resetAuthorizationDataSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('returns callbackContext if all params are good', waitForAsync(() => {
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
|
||||
const expectedCallbackContext = {
|
||||
code: '',
|
||||
refreshToken: '',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { anyHash: '' },
|
||||
isRenewProcess: true,
|
||||
jwtKeys: null,
|
||||
validationResult: null,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
|
||||
const allconfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
},
|
||||
];
|
||||
|
||||
service
|
||||
.implicitFlowCallback(allconfigs[0], allconfigs, 'anyHash')
|
||||
.subscribe((callbackContext) => {
|
||||
expect(callbackContext).toEqual(expectedCallbackContext);
|
||||
});
|
||||
}));
|
||||
|
||||
it('uses window location hash if no hash is passed', waitForAsync(() => {
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
|
||||
const expectedCallbackContext = {
|
||||
code: '',
|
||||
refreshToken: '',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { anyFakeHash: '' },
|
||||
isRenewProcess: true,
|
||||
jwtKeys: null,
|
||||
validationResult: null,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
|
||||
const allconfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
},
|
||||
];
|
||||
|
||||
service
|
||||
.implicitFlowCallback(allconfigs[0], allconfigs)
|
||||
.subscribe((callbackContext) => {
|
||||
expect(callbackContext).toEqual(expectedCallbackContext);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { DOCUMENT } from '../../dom';
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { OpenIdConfiguration } from '../../config/openid-configuration';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { AuthResult, CallbackContext } from '../callback-context';
|
||||
import { FlowsDataService } from '../flows-data.service';
|
||||
import { ResetAuthDataService } from '../reset-auth-data.service';
|
||||
|
||||
@Injectable()
|
||||
export class ImplicitFlowCallbackHandlerService {
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly resetAuthDataService = inject(ResetAuthDataService);
|
||||
|
||||
private readonly flowsDataService = inject(FlowsDataService);
|
||||
|
||||
private readonly document = inject(DOCUMENT);
|
||||
|
||||
// STEP 1 Code Flow
|
||||
// STEP 1 Implicit Flow
|
||||
implicitFlowCallback(
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[],
|
||||
hash?: string
|
||||
): Observable<CallbackContext> {
|
||||
const isRenewProcessData =
|
||||
this.flowsDataService.isSilentRenewRunning(config);
|
||||
|
||||
this.loggerService.logDebug(config, 'BEGIN callback, no auth data');
|
||||
if (!isRenewProcessData) {
|
||||
this.resetAuthDataService.resetAuthorizationData(config, allConfigs);
|
||||
}
|
||||
|
||||
hash = hash || this.document.location.hash.substring(1);
|
||||
|
||||
const authResult = hash
|
||||
.split('&')
|
||||
.reduce((resultData: any, item: string) => {
|
||||
const parts = item.split('=');
|
||||
|
||||
resultData[parts.shift() as string] = parts.join('=');
|
||||
|
||||
return resultData;
|
||||
}, {} as AuthResult);
|
||||
|
||||
const callbackContext: CallbackContext = {
|
||||
code: '',
|
||||
refreshToken: '',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult,
|
||||
isRenewProcess: isRenewProcessData,
|
||||
jwtKeys: null,
|
||||
validationResult: null,
|
||||
existingIdToken: null,
|
||||
};
|
||||
|
||||
return of(callbackContext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { mockProvider } from '../../../test/auto-mock';
|
||||
import { AuthStateService } from '../../auth-state/auth-state.service';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { CallbackContext } from '../callback-context';
|
||||
import { FlowsDataService } from '../flows-data.service';
|
||||
import { RefreshSessionCallbackHandlerService } from './refresh-session-callback-handler.service';
|
||||
|
||||
describe('RefreshSessionCallbackHandlerService', () => {
|
||||
let service: RefreshSessionCallbackHandlerService;
|
||||
let flowsDataService: FlowsDataService;
|
||||
let authStateService: AuthStateService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
RefreshSessionCallbackHandlerService,
|
||||
mockProvider(AuthStateService),
|
||||
mockProvider(LoggerService),
|
||||
mockProvider(FlowsDataService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(RefreshSessionCallbackHandlerService);
|
||||
flowsDataService = TestBed.inject(FlowsDataService);
|
||||
authStateService = TestBed.inject(AuthStateService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('refreshSessionWithRefreshTokens', () => {
|
||||
it('returns callbackContext if all params are good', waitForAsync(() => {
|
||||
spyOn(
|
||||
flowsDataService,
|
||||
'getExistingOrCreateAuthStateControl'
|
||||
).and.returnValue('state-data');
|
||||
spyOn(authStateService, 'getRefreshToken').and.returnValue(
|
||||
'henlo-furiend'
|
||||
);
|
||||
spyOn(authStateService, 'getIdToken').and.returnValue('henlo-legger');
|
||||
|
||||
const expectedCallbackContext = {
|
||||
code: '',
|
||||
refreshToken: 'henlo-furiend',
|
||||
state: 'state-data',
|
||||
sessionState: null,
|
||||
authResult: null,
|
||||
isRenewProcess: true,
|
||||
jwtKeys: null,
|
||||
validationResult: null,
|
||||
existingIdToken: 'henlo-legger',
|
||||
} as CallbackContext;
|
||||
|
||||
service
|
||||
.refreshSessionWithRefreshTokens({ configId: 'configId1' })
|
||||
.subscribe((callbackContext) => {
|
||||
expect(callbackContext).toEqual(expectedCallbackContext);
|
||||
});
|
||||
}));
|
||||
|
||||
it('throws error if no refresh token is given', waitForAsync(() => {
|
||||
spyOn(
|
||||
flowsDataService,
|
||||
'getExistingOrCreateAuthStateControl'
|
||||
).and.returnValue('state-data');
|
||||
spyOn(authStateService, 'getRefreshToken').and.returnValue('');
|
||||
spyOn(authStateService, 'getIdToken').and.returnValue('henlo-legger');
|
||||
|
||||
service
|
||||
.refreshSessionWithRefreshTokens({ configId: 'configId1' })
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Observable, of, throwError } from 'rxjs';
|
||||
import { AuthStateService } from '../../auth-state/auth-state.service';
|
||||
import { OpenIdConfiguration } from '../../config/openid-configuration';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { TokenValidationService } from '../../validation/token-validation.service';
|
||||
import { CallbackContext } from '../callback-context';
|
||||
import { FlowsDataService } from '../flows-data.service';
|
||||
|
||||
@Injectable()
|
||||
export class RefreshSessionCallbackHandlerService {
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly authStateService = inject(AuthStateService);
|
||||
|
||||
private readonly flowsDataService = inject(FlowsDataService);
|
||||
|
||||
// STEP 1 Refresh session
|
||||
refreshSessionWithRefreshTokens(
|
||||
config: OpenIdConfiguration
|
||||
): Observable<CallbackContext> {
|
||||
const stateData =
|
||||
this.flowsDataService.getExistingOrCreateAuthStateControl(config);
|
||||
|
||||
this.loggerService.logDebug(
|
||||
config,
|
||||
'RefreshSession created. Adding myautostate: ' + stateData
|
||||
);
|
||||
const refreshToken = this.authStateService.getRefreshToken(config);
|
||||
const idToken = this.authStateService.getIdToken(config);
|
||||
|
||||
if (refreshToken) {
|
||||
const callbackContext: CallbackContext = {
|
||||
code: '',
|
||||
refreshToken,
|
||||
state: stateData,
|
||||
sessionState: null,
|
||||
authResult: null,
|
||||
isRenewProcess: true,
|
||||
jwtKeys: null,
|
||||
validationResult: null,
|
||||
existingIdToken: idToken,
|
||||
};
|
||||
|
||||
this.loggerService.logDebug(
|
||||
config,
|
||||
'found refresh code, obtaining new credentials with refresh code'
|
||||
);
|
||||
// Nonce is not used with refresh tokens; but Key cloak may send it anyway
|
||||
this.flowsDataService.setNonce(
|
||||
TokenValidationService.refreshTokenNoncePlaceholder,
|
||||
config
|
||||
);
|
||||
|
||||
return of(callbackContext);
|
||||
} else {
|
||||
const errorMessage = 'no refresh token found, please login';
|
||||
|
||||
this.loggerService.logError(config, errorMessage);
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { mockProvider } from '../../../test/auto-mock';
|
||||
import { createRetriableStream } from '../../../test/create-retriable-stream.helper';
|
||||
import { DataService } from '../../api/data.service';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { StoragePersistenceService } from '../../storage/storage-persistence.service';
|
||||
import { UrlService } from '../../utils/url/url.service';
|
||||
import { CallbackContext } from '../callback-context';
|
||||
import { RefreshTokenCallbackHandlerService } from './refresh-token-callback-handler.service';
|
||||
|
||||
describe('RefreshTokenCallbackHandlerService', () => {
|
||||
let service: RefreshTokenCallbackHandlerService;
|
||||
let storagePersistenceService: StoragePersistenceService;
|
||||
let dataService: DataService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
RefreshTokenCallbackHandlerService,
|
||||
mockProvider(UrlService),
|
||||
mockProvider(LoggerService),
|
||||
mockProvider(DataService),
|
||||
mockProvider(StoragePersistenceService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(RefreshTokenCallbackHandlerService);
|
||||
storagePersistenceService = TestBed.inject(StoragePersistenceService);
|
||||
dataService = TestBed.inject(DataService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('refreshTokensRequestTokens', () => {
|
||||
const HTTP_ERROR = new HttpErrorResponse({});
|
||||
const CONNECTION_ERROR = new HttpErrorResponse({
|
||||
error: new ProgressEvent('error'),
|
||||
status: 0,
|
||||
statusText: 'Unknown Error',
|
||||
url: 'https://identity-server.test/openid-connect/token',
|
||||
});
|
||||
|
||||
it('throws error if no tokenEndpoint is given', waitForAsync(() => {
|
||||
(service as any)
|
||||
.refreshTokensRequestTokens({} as CallbackContext)
|
||||
.subscribe({
|
||||
error: (err: unknown) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls data service if all params are good', waitForAsync(() => {
|
||||
const postSpy = spyOn(dataService, 'post').and.returnValue(of({}));
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', { configId: 'configId1' })
|
||||
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
|
||||
|
||||
service
|
||||
.refreshTokensRequestTokens({} as CallbackContext, {
|
||||
configId: 'configId1',
|
||||
})
|
||||
.subscribe(() => {
|
||||
expect(postSpy).toHaveBeenCalledOnceWith(
|
||||
'tokenEndpoint',
|
||||
undefined,
|
||||
{ configId: 'configId1' },
|
||||
jasmine.any(HttpHeaders)
|
||||
);
|
||||
const httpHeaders = postSpy.calls.mostRecent().args[3] as HttpHeaders;
|
||||
|
||||
expect(httpHeaders.has('Content-Type')).toBeTrue();
|
||||
expect(httpHeaders.get('Content-Type')).toBe(
|
||||
'application/x-www-form-urlencoded'
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls data service with correct headers if all params are good', waitForAsync(() => {
|
||||
const postSpy = spyOn(dataService, 'post').and.returnValue(of({}));
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', { configId: 'configId1' })
|
||||
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
|
||||
|
||||
service
|
||||
.refreshTokensRequestTokens({} as CallbackContext, {
|
||||
configId: 'configId1',
|
||||
})
|
||||
.subscribe(() => {
|
||||
const httpHeaders = postSpy.calls.mostRecent().args[3] as HttpHeaders;
|
||||
|
||||
expect(httpHeaders.has('Content-Type')).toBeTrue();
|
||||
expect(httpHeaders.get('Content-Type')).toBe(
|
||||
'application/x-www-form-urlencoded'
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
it('returns error in case of http error', waitForAsync(() => {
|
||||
spyOn(dataService, 'post').and.returnValue(throwError(() => HTTP_ERROR));
|
||||
const config = { configId: 'configId1', authority: 'authority' };
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', config)
|
||||
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
|
||||
|
||||
service
|
||||
.refreshTokensRequestTokens({} as CallbackContext, config)
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('retries request in case of no connection http error and succeeds', waitForAsync(() => {
|
||||
const postSpy = spyOn(dataService, 'post').and.returnValue(
|
||||
createRetriableStream(
|
||||
throwError(() => CONNECTION_ERROR),
|
||||
of({})
|
||||
)
|
||||
);
|
||||
const config = { configId: 'configId1', authority: 'authority' };
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', config)
|
||||
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
|
||||
|
||||
service
|
||||
.refreshTokensRequestTokens({} as CallbackContext, config)
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
expect(res).toBeTruthy();
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
error: (err) => {
|
||||
// fails if there should be a result
|
||||
expect(err).toBeFalsy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('retries request in case of no connection http error and fails because of http error afterwards', waitForAsync(() => {
|
||||
const postSpy = spyOn(dataService, 'post').and.returnValue(
|
||||
createRetriableStream(
|
||||
throwError(() => CONNECTION_ERROR),
|
||||
throwError(() => HTTP_ERROR)
|
||||
)
|
||||
);
|
||||
const config = { configId: 'configId1', authority: 'authority' };
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', config)
|
||||
.and.returnValue({ tokenEndpoint: 'tokenEndpoint' });
|
||||
|
||||
service
|
||||
.refreshTokensRequestTokens({} as CallbackContext, config)
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
// fails if there should be a result
|
||||
expect(res).toBeFalsy();
|
||||
},
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
expect(postSpy).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { HttpHeaders } from '@ngify/http';
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Observable, of, throwError, timer } from 'rxjs';
|
||||
import { catchError, mergeMap, retryWhen, switchMap } from 'rxjs/operators';
|
||||
import { DataService } from '../../api/data.service';
|
||||
import { OpenIdConfiguration } from '../../config/openid-configuration';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { StoragePersistenceService } from '../../storage/storage-persistence.service';
|
||||
import { UrlService } from '../../utils/url/url.service';
|
||||
import { AuthResult, CallbackContext } from '../callback-context';
|
||||
import { isNetworkError } from './error-helper';
|
||||
|
||||
@Injectable()
|
||||
export class RefreshTokenCallbackHandlerService {
|
||||
private readonly urlService = inject(UrlService);
|
||||
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly dataService = inject(DataService);
|
||||
|
||||
private readonly storagePersistenceService = inject(
|
||||
StoragePersistenceService
|
||||
);
|
||||
|
||||
// STEP 2 Refresh Token
|
||||
refreshTokensRequestTokens(
|
||||
callbackContext: CallbackContext,
|
||||
config: OpenIdConfiguration,
|
||||
customParamsRefresh?: { [key: string]: string | number | boolean }
|
||||
): Observable<CallbackContext> {
|
||||
let headers: HttpHeaders = new HttpHeaders();
|
||||
|
||||
headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
||||
|
||||
const authWellknownEndpoints = this.storagePersistenceService.read(
|
||||
'authWellKnownEndPoints',
|
||||
config
|
||||
);
|
||||
const tokenEndpoint = authWellknownEndpoints?.tokenEndpoint;
|
||||
|
||||
if (!tokenEndpoint) {
|
||||
return throwError(() => new Error('Token Endpoint not defined'));
|
||||
}
|
||||
|
||||
const data = this.urlService.createBodyForCodeFlowRefreshTokensRequest(
|
||||
callbackContext.refreshToken,
|
||||
config,
|
||||
customParamsRefresh
|
||||
);
|
||||
|
||||
return this.dataService
|
||||
.post<AuthResult>(tokenEndpoint, data, config, headers)
|
||||
.pipe(
|
||||
switchMap((response) => {
|
||||
this.loggerService.logDebug(
|
||||
config,
|
||||
`token refresh response: ${response}`
|
||||
);
|
||||
|
||||
if (response) {
|
||||
response.state = callbackContext.state;
|
||||
}
|
||||
|
||||
callbackContext.authResult = response;
|
||||
|
||||
return of(callbackContext);
|
||||
}),
|
||||
retryWhen((error) => this.handleRefreshRetry(error, config)),
|
||||
catchError((error) => {
|
||||
const { authority } = config;
|
||||
const errorMessage = `OidcService code request ${authority}`;
|
||||
|
||||
this.loggerService.logError(config, errorMessage, error);
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private handleRefreshRetry(
|
||||
errors: Observable<unknown>,
|
||||
config: OpenIdConfiguration
|
||||
): Observable<unknown> {
|
||||
return errors.pipe(
|
||||
mergeMap((error) => {
|
||||
// retry token refresh if there is no internet connection
|
||||
if (isNetworkError(error)) {
|
||||
const { authority, refreshTokenRetryInSeconds } = config;
|
||||
const errorMessage = `OidcService code request ${authority} - no internet connection`;
|
||||
|
||||
this.loggerService.logWarning(config, errorMessage, error);
|
||||
|
||||
return timer((refreshTokenRetryInSeconds ?? 0) * 1000);
|
||||
}
|
||||
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { DOCUMENT } from '../../dom';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { mockProvider } from '../../../test/auto-mock';
|
||||
import { AuthStateService } from '../../auth-state/auth-state.service';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { StateValidationResult } from '../../validation/state-validation-result';
|
||||
import { StateValidationService } from '../../validation/state-validation.service';
|
||||
import { ValidationResult } from '../../validation/validation-result';
|
||||
import { CallbackContext } from '../callback-context';
|
||||
import { ResetAuthDataService } from '../reset-auth-data.service';
|
||||
import { StateValidationCallbackHandlerService } from './state-validation-callback-handler.service';
|
||||
|
||||
describe('StateValidationCallbackHandlerService', () => {
|
||||
let service: StateValidationCallbackHandlerService;
|
||||
let stateValidationService: StateValidationService;
|
||||
let loggerService: LoggerService;
|
||||
let authStateService: AuthStateService;
|
||||
let resetAuthDataService: ResetAuthDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
StateValidationCallbackHandlerService,
|
||||
mockProvider(LoggerService),
|
||||
mockProvider(StateValidationService),
|
||||
mockProvider(AuthStateService),
|
||||
mockProvider(ResetAuthDataService),
|
||||
{
|
||||
provide: DOCUMENT,
|
||||
useValue: {
|
||||
location: {
|
||||
get hash(): string {
|
||||
return '&anyFakeHash';
|
||||
},
|
||||
set hash(_value) {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(StateValidationCallbackHandlerService);
|
||||
stateValidationService = TestBed.inject(StateValidationService);
|
||||
loggerService = TestBed.inject(LoggerService);
|
||||
authStateService = TestBed.inject(AuthStateService);
|
||||
resetAuthDataService = TestBed.inject(ResetAuthDataService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('callbackStateValidation', () => {
|
||||
it('returns callbackContext with validationResult if validationResult is valid', waitForAsync(() => {
|
||||
spyOn(stateValidationService, 'getValidatedStateResult').and.returnValue(
|
||||
of({
|
||||
idToken: 'idTokenJustForTesting',
|
||||
authResponseIsValid: true,
|
||||
} as StateValidationResult)
|
||||
);
|
||||
const allConfigs = [{ configId: 'configId1' }];
|
||||
|
||||
service
|
||||
.callbackStateValidation(
|
||||
{} as CallbackContext,
|
||||
allConfigs[0],
|
||||
allConfigs
|
||||
)
|
||||
.subscribe((newCallbackContext) => {
|
||||
expect(newCallbackContext).toEqual({
|
||||
validationResult: {
|
||||
idToken: 'idTokenJustForTesting',
|
||||
authResponseIsValid: true,
|
||||
},
|
||||
} as CallbackContext);
|
||||
});
|
||||
}));
|
||||
|
||||
it('logs error in case of an error', waitForAsync(() => {
|
||||
spyOn(stateValidationService, 'getValidatedStateResult').and.returnValue(
|
||||
of({
|
||||
authResponseIsValid: false,
|
||||
} as StateValidationResult)
|
||||
);
|
||||
|
||||
const loggerSpy = spyOn(loggerService, 'logWarning');
|
||||
const allConfigs = [{ configId: 'configId1' }];
|
||||
|
||||
service
|
||||
.callbackStateValidation(
|
||||
{} as CallbackContext,
|
||||
allConfigs[0],
|
||||
allConfigs
|
||||
)
|
||||
.subscribe({
|
||||
error: () => {
|
||||
expect(loggerSpy).toHaveBeenCalledOnceWith(
|
||||
allConfigs[0],
|
||||
'authorizedCallback, token(s) validation failed, resetting. Hash: &anyFakeHash'
|
||||
);
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls resetAuthDataService.resetAuthorizationData and authStateService.updateAndPublishAuthState in case of an error', waitForAsync(() => {
|
||||
spyOn(stateValidationService, 'getValidatedStateResult').and.returnValue(
|
||||
of({
|
||||
authResponseIsValid: false,
|
||||
state: ValidationResult.LoginRequired,
|
||||
} as StateValidationResult)
|
||||
);
|
||||
|
||||
const resetAuthorizationDataSpy = spyOn(
|
||||
resetAuthDataService,
|
||||
'resetAuthorizationData'
|
||||
);
|
||||
const updateAndPublishAuthStateSpy = spyOn(
|
||||
authStateService,
|
||||
'updateAndPublishAuthState'
|
||||
);
|
||||
const allConfigs = [{ configId: 'configId1' }];
|
||||
|
||||
service
|
||||
.callbackStateValidation(
|
||||
{ isRenewProcess: true } as CallbackContext,
|
||||
allConfigs[0],
|
||||
allConfigs
|
||||
)
|
||||
.subscribe({
|
||||
error: () => {
|
||||
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
|
||||
isAuthenticated: false,
|
||||
validationResult: ValidationResult.LoginRequired,
|
||||
isRenewProcess: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { DOCUMENT } from '../../dom';
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { AuthStateService } from '../../auth-state/auth-state.service';
|
||||
import { OpenIdConfiguration } from '../../config/openid-configuration';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { StateValidationResult } from '../../validation/state-validation-result';
|
||||
import { StateValidationService } from '../../validation/state-validation.service';
|
||||
import { CallbackContext } from '../callback-context';
|
||||
import { ResetAuthDataService } from '../reset-auth-data.service';
|
||||
|
||||
@Injectable()
|
||||
export class StateValidationCallbackHandlerService {
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly stateValidationService = inject(StateValidationService);
|
||||
|
||||
private readonly authStateService = inject(AuthStateService);
|
||||
|
||||
private readonly resetAuthDataService = inject(ResetAuthDataService);
|
||||
|
||||
private readonly document = inject(DOCUMENT);
|
||||
|
||||
// STEP 4 All flows
|
||||
callbackStateValidation(
|
||||
callbackContext: CallbackContext,
|
||||
configuration: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[]
|
||||
): Observable<CallbackContext> {
|
||||
return this.stateValidationService
|
||||
.getValidatedStateResult(callbackContext, configuration)
|
||||
.pipe(
|
||||
map((validationResult: StateValidationResult) => {
|
||||
callbackContext.validationResult = validationResult;
|
||||
|
||||
if (validationResult.authResponseIsValid) {
|
||||
this.authStateService.setAuthorizationData(
|
||||
validationResult.accessToken,
|
||||
callbackContext.authResult,
|
||||
configuration,
|
||||
allConfigs
|
||||
);
|
||||
|
||||
return callbackContext;
|
||||
} else {
|
||||
const errorMessage = `authorizedCallback, token(s) validation failed, resetting. Hash: ${this.document.location.hash}`;
|
||||
|
||||
this.loggerService.logWarning(configuration, errorMessage);
|
||||
this.resetAuthDataService.resetAuthorizationData(
|
||||
configuration,
|
||||
allConfigs
|
||||
);
|
||||
this.publishUnauthorizedState(
|
||||
callbackContext.validationResult,
|
||||
callbackContext.isRenewProcess
|
||||
);
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private publishUnauthorizedState(
|
||||
stateValidationResult: StateValidationResult,
|
||||
isRenewProcess: boolean
|
||||
): void {
|
||||
this.authStateService.updateAndPublishAuthState({
|
||||
isAuthenticated: false,
|
||||
validationResult: stateValidationResult.state,
|
||||
isRenewProcess,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { mockProvider } from '../../../test/auto-mock';
|
||||
import { AuthStateService } from '../../auth-state/auth-state.service';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { UserService } from '../../user-data/user.service';
|
||||
import { StateValidationResult } from '../../validation/state-validation-result';
|
||||
import { ValidationResult } from '../../validation/validation-result';
|
||||
import { CallbackContext } from '../callback-context';
|
||||
import { FlowsDataService } from '../flows-data.service';
|
||||
import { ResetAuthDataService } from '../reset-auth-data.service';
|
||||
import { UserCallbackHandlerService } from './user-callback-handler.service';
|
||||
|
||||
describe('UserCallbackHandlerService', () => {
|
||||
let service: UserCallbackHandlerService;
|
||||
let authStateService: AuthStateService;
|
||||
let flowsDataService: FlowsDataService;
|
||||
let userService: UserService;
|
||||
let resetAuthDataService: ResetAuthDataService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
UserCallbackHandlerService,
|
||||
mockProvider(LoggerService),
|
||||
mockProvider(AuthStateService),
|
||||
mockProvider(FlowsDataService),
|
||||
mockProvider(UserService),
|
||||
mockProvider(ResetAuthDataService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(UserCallbackHandlerService);
|
||||
flowsDataService = TestBed.inject(FlowsDataService);
|
||||
authStateService = TestBed.inject(AuthStateService);
|
||||
userService = TestBed.inject(UserService);
|
||||
resetAuthDataService = TestBed.inject(ResetAuthDataService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('callbackUser', () => {
|
||||
it('calls flowsDataService.setSessionState with correct params if autoUserInfo is false, isRenewProcess is false and refreshToken is null', waitForAsync(() => {
|
||||
const svr = new StateValidationResult(
|
||||
'accesstoken',
|
||||
'idtoken',
|
||||
true,
|
||||
'decoded'
|
||||
);
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: '',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { session_state: 'mystate' },
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: svr,
|
||||
existingIdToken: '',
|
||||
} as CallbackContext;
|
||||
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
autoUserInfo: false,
|
||||
},
|
||||
];
|
||||
|
||||
const spy = spyOn(flowsDataService, 'setSessionState');
|
||||
|
||||
service
|
||||
.callbackUser(callbackContext, allConfigs[0], allConfigs)
|
||||
.subscribe((resultCallbackContext) => {
|
||||
expect(spy).toHaveBeenCalledOnceWith('mystate', allConfigs[0]);
|
||||
expect(resultCallbackContext).toEqual(callbackContext);
|
||||
});
|
||||
}));
|
||||
|
||||
it('does NOT call flowsDataService.setSessionState if autoUserInfo is false, isRenewProcess is true and refreshToken is null', waitForAsync(() => {
|
||||
const svr = new StateValidationResult(
|
||||
'accesstoken',
|
||||
'idtoken',
|
||||
true,
|
||||
'decoded'
|
||||
);
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: '',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { session_state: 'mystate' },
|
||||
isRenewProcess: true,
|
||||
jwtKeys: null,
|
||||
validationResult: svr,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
autoUserInfo: false,
|
||||
},
|
||||
];
|
||||
const spy = spyOn(flowsDataService, 'setSessionState');
|
||||
|
||||
service
|
||||
.callbackUser(callbackContext, allConfigs[0], allConfigs)
|
||||
.subscribe((resultCallbackContext) => {
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(resultCallbackContext).toEqual(callbackContext);
|
||||
});
|
||||
}));
|
||||
|
||||
it('does NOT call flowsDataService.setSessionState if autoUserInfo is false isRenewProcess is false, refreshToken has value', waitForAsync(() => {
|
||||
const svr = new StateValidationResult(
|
||||
'accesstoken',
|
||||
'idtoken',
|
||||
true,
|
||||
'decoded'
|
||||
);
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: 'somerefreshtoken',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { session_state: 'mystate' },
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: svr,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
autoUserInfo: false,
|
||||
},
|
||||
];
|
||||
const spy = spyOn(flowsDataService, 'setSessionState');
|
||||
|
||||
service
|
||||
.callbackUser(callbackContext, allConfigs[0], allConfigs)
|
||||
.subscribe((resultCallbackContext) => {
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(resultCallbackContext).toEqual(callbackContext);
|
||||
});
|
||||
}));
|
||||
|
||||
it('does NOT call flowsDataService.setSessionState if autoUserInfo is false isRenewProcess is false, refreshToken has value, id_token is false', waitForAsync(() => {
|
||||
const svr = new StateValidationResult('accesstoken', '', true, '');
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: 'somerefreshtoken',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { session_state: 'mystate' },
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: svr,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
autoUserInfo: false,
|
||||
},
|
||||
];
|
||||
|
||||
const spy = spyOn(flowsDataService, 'setSessionState');
|
||||
|
||||
service
|
||||
.callbackUser(callbackContext, allConfigs[0], allConfigs)
|
||||
.subscribe((resultCallbackContext) => {
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
expect(resultCallbackContext).toEqual(callbackContext);
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls authStateService.updateAndPublishAuthState with correct params if autoUserInfo is false', waitForAsync(() => {
|
||||
const svr = new StateValidationResult(
|
||||
'accesstoken',
|
||||
'idtoken',
|
||||
true,
|
||||
'decoded'
|
||||
);
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: 'somerefreshtoken',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { session_state: 'mystate' },
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: svr,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
autoUserInfo: false,
|
||||
},
|
||||
];
|
||||
|
||||
const updateAndPublishAuthStateSpy = spyOn(
|
||||
authStateService,
|
||||
'updateAndPublishAuthState'
|
||||
);
|
||||
|
||||
service
|
||||
.callbackUser(callbackContext, allConfigs[0], allConfigs)
|
||||
.subscribe((resultCallbackContext) => {
|
||||
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
|
||||
isAuthenticated: true,
|
||||
validationResult: ValidationResult.NotSet,
|
||||
isRenewProcess: false,
|
||||
});
|
||||
expect(resultCallbackContext).toEqual(callbackContext);
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls userService.getAndPersistUserDataInStore with correct params if autoUserInfo is true', waitForAsync(() => {
|
||||
const svr = new StateValidationResult(
|
||||
'accesstoken',
|
||||
'idtoken',
|
||||
true,
|
||||
'decoded'
|
||||
);
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: 'somerefreshtoken',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { session_state: 'mystate' },
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: svr,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
autoUserInfo: true,
|
||||
},
|
||||
];
|
||||
|
||||
const getAndPersistUserDataInStoreSpy = spyOn(
|
||||
userService,
|
||||
'getAndPersistUserDataInStore'
|
||||
).and.returnValue(of({ user: 'some_data' }));
|
||||
|
||||
service
|
||||
.callbackUser(callbackContext, allConfigs[0], allConfigs)
|
||||
.subscribe((resultCallbackContext) => {
|
||||
expect(getAndPersistUserDataInStoreSpy).toHaveBeenCalledOnceWith(
|
||||
allConfigs[0],
|
||||
allConfigs,
|
||||
false,
|
||||
'idtoken',
|
||||
'decoded'
|
||||
);
|
||||
expect(resultCallbackContext).toEqual(callbackContext);
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls authStateService.updateAndPublishAuthState with correct params if autoUserInfo is true', waitForAsync(() => {
|
||||
const svr = new StateValidationResult(
|
||||
'accesstoken',
|
||||
'idtoken',
|
||||
true,
|
||||
'decoded',
|
||||
ValidationResult.MaxOffsetExpired
|
||||
);
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: 'somerefreshtoken',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { session_state: 'mystate' },
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: svr,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
autoUserInfo: true,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
|
||||
of({ user: 'some_data' })
|
||||
);
|
||||
const updateAndPublishAuthStateSpy = spyOn(
|
||||
authStateService,
|
||||
'updateAndPublishAuthState'
|
||||
);
|
||||
|
||||
service
|
||||
.callbackUser(callbackContext, allConfigs[0], allConfigs)
|
||||
.subscribe((resultCallbackContext) => {
|
||||
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
|
||||
isAuthenticated: true,
|
||||
validationResult: ValidationResult.MaxOffsetExpired,
|
||||
isRenewProcess: false,
|
||||
});
|
||||
expect(resultCallbackContext).toEqual(callbackContext);
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls flowsDataService.setSessionState with correct params if user data is present and NOT refresh token', waitForAsync(() => {
|
||||
const svr = new StateValidationResult(
|
||||
'accesstoken',
|
||||
'idtoken',
|
||||
true,
|
||||
'decoded',
|
||||
ValidationResult.MaxOffsetExpired
|
||||
);
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: '', // something falsy
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { session_state: 'mystate' },
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: svr,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
autoUserInfo: true,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
|
||||
of({ user: 'some_data' })
|
||||
);
|
||||
const setSessionStateSpy = spyOn(flowsDataService, 'setSessionState');
|
||||
|
||||
service
|
||||
.callbackUser(callbackContext, allConfigs[0], allConfigs)
|
||||
.subscribe((resultCallbackContext) => {
|
||||
expect(setSessionStateSpy).toHaveBeenCalledOnceWith(
|
||||
'mystate',
|
||||
allConfigs[0]
|
||||
);
|
||||
expect(resultCallbackContext).toEqual(callbackContext);
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls authStateService.publishUnauthorizedState with correct params if user info which are coming back are null', waitForAsync(() => {
|
||||
const svr = new StateValidationResult(
|
||||
'accesstoken',
|
||||
'idtoken',
|
||||
true,
|
||||
'decoded',
|
||||
ValidationResult.MaxOffsetExpired
|
||||
);
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: 'somerefreshtoken',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { session_state: 'mystate' },
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: svr,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
autoUserInfo: true,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
|
||||
of(null)
|
||||
);
|
||||
const updateAndPublishAuthStateSpy = spyOn(
|
||||
authStateService,
|
||||
'updateAndPublishAuthState'
|
||||
);
|
||||
|
||||
service
|
||||
.callbackUser(callbackContext, allConfigs[0], allConfigs)
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(updateAndPublishAuthStateSpy).toHaveBeenCalledOnceWith({
|
||||
isAuthenticated: false,
|
||||
validationResult: ValidationResult.MaxOffsetExpired,
|
||||
isRenewProcess: false,
|
||||
});
|
||||
expect(err.message).toEqual(
|
||||
'Failed to retrieve user info with error: Error: Called for userData but they were null'
|
||||
);
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls resetAuthDataService.resetAuthorizationData if user info which are coming back are null', waitForAsync(() => {
|
||||
const svr = new StateValidationResult(
|
||||
'accesstoken',
|
||||
'idtoken',
|
||||
true,
|
||||
'decoded',
|
||||
ValidationResult.MaxOffsetExpired
|
||||
);
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: 'somerefreshtoken',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: { session_state: 'mystate' },
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: svr,
|
||||
existingIdToken: null,
|
||||
} as CallbackContext;
|
||||
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
autoUserInfo: true,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(userService, 'getAndPersistUserDataInStore').and.returnValue(
|
||||
of(null)
|
||||
);
|
||||
const resetAuthorizationDataSpy = spyOn(
|
||||
resetAuthDataService,
|
||||
'resetAuthorizationData'
|
||||
);
|
||||
|
||||
service
|
||||
.callbackUser(callbackContext, allConfigs[0], allConfigs)
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
|
||||
expect(err.message).toEqual(
|
||||
'Failed to retrieve user info with error: Error: Called for userData but they were null'
|
||||
);
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
132
src/flows/callback-handling/user-callback-handler.service.ts
Normal file
132
src/flows/callback-handling/user-callback-handler.service.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Observable, of, throwError } from 'rxjs';
|
||||
import { catchError, switchMap } from 'rxjs/operators';
|
||||
import { AuthStateService } from '../../auth-state/auth-state.service';
|
||||
import { OpenIdConfiguration } from '../../config/openid-configuration';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { UserService } from '../../user-data/user.service';
|
||||
import { StateValidationResult } from '../../validation/state-validation-result';
|
||||
import { CallbackContext } from '../callback-context';
|
||||
import { FlowsDataService } from '../flows-data.service';
|
||||
import { ResetAuthDataService } from '../reset-auth-data.service';
|
||||
|
||||
@Injectable()
|
||||
export class UserCallbackHandlerService {
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly authStateService = inject(AuthStateService);
|
||||
|
||||
private readonly flowsDataService = inject(FlowsDataService);
|
||||
|
||||
private readonly userService = inject(UserService);
|
||||
|
||||
private readonly resetAuthDataService = inject(ResetAuthDataService);
|
||||
|
||||
// STEP 5 userData
|
||||
callbackUser(
|
||||
callbackContext: CallbackContext,
|
||||
configuration: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[]
|
||||
): Observable<CallbackContext> {
|
||||
const { isRenewProcess, validationResult, authResult, refreshToken } =
|
||||
callbackContext;
|
||||
const { autoUserInfo, renewUserInfoAfterTokenRenew } = configuration;
|
||||
|
||||
if (!autoUserInfo) {
|
||||
if (!isRenewProcess || renewUserInfoAfterTokenRenew) {
|
||||
// userData is set to the id_token decoded, auto get user data set to false
|
||||
if (validationResult?.decodedIdToken) {
|
||||
this.userService.setUserDataToStore(
|
||||
validationResult.decodedIdToken,
|
||||
configuration,
|
||||
allConfigs
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isRenewProcess && !refreshToken) {
|
||||
this.flowsDataService.setSessionState(
|
||||
authResult?.session_state,
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
this.publishAuthState(validationResult, isRenewProcess);
|
||||
|
||||
return of(callbackContext);
|
||||
}
|
||||
|
||||
return this.userService
|
||||
.getAndPersistUserDataInStore(
|
||||
configuration,
|
||||
allConfigs,
|
||||
isRenewProcess,
|
||||
validationResult?.idToken,
|
||||
validationResult?.decodedIdToken
|
||||
)
|
||||
.pipe(
|
||||
switchMap((userData) => {
|
||||
if (!!userData) {
|
||||
if (!refreshToken) {
|
||||
this.flowsDataService.setSessionState(
|
||||
authResult?.session_state,
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
this.publishAuthState(validationResult, isRenewProcess);
|
||||
|
||||
return of(callbackContext);
|
||||
} else {
|
||||
this.resetAuthDataService.resetAuthorizationData(
|
||||
configuration,
|
||||
allConfigs
|
||||
);
|
||||
this.publishUnauthenticatedState(validationResult, isRenewProcess);
|
||||
const errorMessage = `Called for userData but they were ${userData}`;
|
||||
|
||||
this.loggerService.logWarning(configuration, errorMessage);
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
}
|
||||
}),
|
||||
catchError((err) => {
|
||||
const errorMessage = `Failed to retrieve user info with error: ${err}`;
|
||||
|
||||
this.loggerService.logWarning(configuration, errorMessage);
|
||||
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private publishAuthState(
|
||||
stateValidationResult: StateValidationResult | null,
|
||||
isRenewProcess: boolean
|
||||
): void {
|
||||
if (!stateValidationResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.authStateService.updateAndPublishAuthState({
|
||||
isAuthenticated: true,
|
||||
validationResult: stateValidationResult.state,
|
||||
isRenewProcess,
|
||||
});
|
||||
}
|
||||
|
||||
private publishUnauthenticatedState(
|
||||
stateValidationResult: StateValidationResult | null,
|
||||
isRenewProcess: boolean
|
||||
): void {
|
||||
if (!stateValidationResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.authStateService.updateAndPublishAuthState({
|
||||
isAuthenticated: false,
|
||||
validationResult: stateValidationResult.state,
|
||||
isRenewProcess,
|
||||
});
|
||||
}
|
||||
}
|
||||
333
src/flows/flows-data.service.spec.ts
Normal file
333
src/flows/flows-data.service.spec.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { mockProvider } from '../../test/auto-mock';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { StoragePersistenceService } from '../storage/storage-persistence.service';
|
||||
import { CryptoService } from '../utils/crypto/crypto.service';
|
||||
import { FlowsDataService } from './flows-data.service';
|
||||
import { RandomService } from './random/random.service';
|
||||
|
||||
describe('Flows Data Service', () => {
|
||||
let service: FlowsDataService;
|
||||
let storagePersistenceService: StoragePersistenceService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
FlowsDataService,
|
||||
RandomService,
|
||||
CryptoService,
|
||||
mockProvider(LoggerService),
|
||||
mockProvider(StoragePersistenceService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(FlowsDataService);
|
||||
storagePersistenceService = TestBed.inject(StoragePersistenceService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('createNonce', () => {
|
||||
it('createNonce returns nonce and stores it', () => {
|
||||
const spy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
const result = service.createNonce({ configId: 'configId1' });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(spy).toHaveBeenCalledOnceWith('authNonce', result, {
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AuthStateControl', () => {
|
||||
it('getAuthStateControl returns property from store', () => {
|
||||
const spy = spyOn(storagePersistenceService, 'read');
|
||||
|
||||
service.getAuthStateControl({ configId: 'configId1' });
|
||||
|
||||
expect(spy).toHaveBeenCalledOnceWith('authStateControl', {
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
|
||||
it('setAuthStateControl saves property in store', () => {
|
||||
const spy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
service.setAuthStateControl('ToSave', { configId: 'configId1' });
|
||||
|
||||
expect(spy).toHaveBeenCalledOnceWith('authStateControl', 'ToSave', {
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExistingOrCreateAuthStateControl', () => {
|
||||
it('if nothing stored it creates a 40 char one and saves the authStateControl', () => {
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authStateControl', { configId: 'configId1' })
|
||||
.and.returnValue(null);
|
||||
const setSpy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
const result = service.getExistingOrCreateAuthStateControl({
|
||||
configId: 'configId1',
|
||||
});
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.length).toBe(41);
|
||||
expect(setSpy).toHaveBeenCalledOnceWith('authStateControl', result, {
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
|
||||
it('if stored it returns the value and does NOT Store the value again', () => {
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authStateControl', { configId: 'configId1' })
|
||||
.and.returnValue('someAuthStateControl');
|
||||
const setSpy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
const result = service.getExistingOrCreateAuthStateControl({
|
||||
configId: 'configId1',
|
||||
});
|
||||
|
||||
expect(result).toEqual('someAuthStateControl');
|
||||
expect(result.length).toBe('someAuthStateControl'.length);
|
||||
expect(setSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSessionState', () => {
|
||||
it('setSessionState saves the value in the storage', () => {
|
||||
const spy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
service.setSessionState('Genesis', { configId: 'configId1' });
|
||||
|
||||
expect(spy).toHaveBeenCalledOnceWith('session_state', 'Genesis', {
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetStorageFlowData', () => {
|
||||
it('resetStorageFlowData calls correct method on storagePersistenceService', () => {
|
||||
const spy = spyOn(storagePersistenceService, 'resetStorageFlowData');
|
||||
|
||||
service.resetStorageFlowData({ configId: 'configId1' });
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('codeVerifier', () => {
|
||||
it('getCodeVerifier returns value from the store', () => {
|
||||
const spy = spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('codeVerifier', { configId: 'configId1' })
|
||||
.and.returnValue('Genesis');
|
||||
|
||||
const result = service.getCodeVerifier({ configId: 'configId1' });
|
||||
|
||||
expect(result).toBe('Genesis');
|
||||
expect(spy).toHaveBeenCalledOnceWith('codeVerifier', {
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
|
||||
it('createCodeVerifier returns random createCodeVerifier and stores it', () => {
|
||||
const setSpy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
const result = service.createCodeVerifier({ configId: 'configId1' });
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
expect(result.length).toBe(67);
|
||||
expect(setSpy).toHaveBeenCalledOnceWith('codeVerifier', result, {
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCodeFlowInProgress', () => {
|
||||
it('checks code flow is in progress and returns result', () => {
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
};
|
||||
|
||||
jasmine.clock().uninstall();
|
||||
jasmine.clock().install();
|
||||
const baseTime = new Date();
|
||||
|
||||
jasmine.clock().mockDate(baseTime);
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('storageCodeFlowInProgress', config)
|
||||
.and.returnValue(true);
|
||||
const spyWrite = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
const isCodeFlowInProgressResult = service.isCodeFlowInProgress(config);
|
||||
|
||||
expect(spyWrite).not.toHaveBeenCalled();
|
||||
expect(isCodeFlowInProgressResult).toBeTrue();
|
||||
});
|
||||
|
||||
it('state object does not exist returns false result', () => {
|
||||
// arrange
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('storageCodeFlowInProgress', { configId: 'configId1' })
|
||||
.and.returnValue(null);
|
||||
|
||||
// act
|
||||
const isCodeFlowInProgressResult = service.isCodeFlowInProgress({
|
||||
configId: 'configId1',
|
||||
});
|
||||
|
||||
// assert
|
||||
expect(isCodeFlowInProgressResult).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCodeFlowInProgress', () => {
|
||||
it('set setCodeFlowInProgress to `in progress` when called', () => {
|
||||
jasmine.clock().uninstall();
|
||||
jasmine.clock().install();
|
||||
const baseTime = new Date();
|
||||
|
||||
jasmine.clock().mockDate(baseTime);
|
||||
|
||||
const spy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
service.setCodeFlowInProgress({ configId: 'configId1' });
|
||||
expect(spy).toHaveBeenCalledOnceWith('storageCodeFlowInProgress', true, {
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetCodeFlowInProgress', () => {
|
||||
it('set resetCodeFlowInProgress to false when called', () => {
|
||||
const spy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
service.resetCodeFlowInProgress({ configId: 'configId1' });
|
||||
expect(spy).toHaveBeenCalledOnceWith('storageCodeFlowInProgress', false, {
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSilentRenewRunning', () => {
|
||||
it('silent renew process timeout exceeded reset state object and returns false result', () => {
|
||||
const config = {
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
configId: 'configId1',
|
||||
};
|
||||
|
||||
jasmine.clock().uninstall();
|
||||
jasmine.clock().install();
|
||||
const baseTime = new Date();
|
||||
|
||||
jasmine.clock().mockDate(baseTime);
|
||||
|
||||
const storageObject = {
|
||||
state: 'running',
|
||||
dateOfLaunchedProcessUtc: baseTime.toISOString(),
|
||||
};
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('storageSilentRenewRunning', config)
|
||||
.and.returnValue(JSON.stringify(storageObject));
|
||||
const spyWrite = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
jasmine.clock().tick((config.silentRenewTimeoutInSeconds + 1) * 1000);
|
||||
|
||||
const isSilentRenewRunningResult = service.isSilentRenewRunning(config);
|
||||
|
||||
expect(spyWrite).toHaveBeenCalledOnceWith(
|
||||
'storageSilentRenewRunning',
|
||||
'',
|
||||
config
|
||||
);
|
||||
expect(isSilentRenewRunningResult).toBeFalse();
|
||||
});
|
||||
|
||||
it('checks silent renew process and returns result', () => {
|
||||
const config = {
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
configId: 'configId1',
|
||||
};
|
||||
|
||||
jasmine.clock().uninstall();
|
||||
jasmine.clock().install();
|
||||
const baseTime = new Date();
|
||||
|
||||
jasmine.clock().mockDate(baseTime);
|
||||
|
||||
const storageObject = {
|
||||
state: 'running',
|
||||
dateOfLaunchedProcessUtc: baseTime.toISOString(),
|
||||
};
|
||||
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('storageSilentRenewRunning', config)
|
||||
.and.returnValue(JSON.stringify(storageObject));
|
||||
const spyWrite = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
const isSilentRenewRunningResult = service.isSilentRenewRunning(config);
|
||||
|
||||
expect(spyWrite).not.toHaveBeenCalled();
|
||||
expect(isSilentRenewRunningResult).toBeTrue();
|
||||
});
|
||||
|
||||
it('state object does not exist returns false result', () => {
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('storageSilentRenewRunning', { configId: 'configId1' })
|
||||
.and.returnValue(null);
|
||||
|
||||
const isSilentRenewRunningResult = service.isSilentRenewRunning({
|
||||
configId: 'configId1',
|
||||
});
|
||||
|
||||
expect(isSilentRenewRunningResult).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSilentRenewRunning', () => {
|
||||
it('set setSilentRenewRunning to `running` with lauched time when called', () => {
|
||||
jasmine.clock().uninstall();
|
||||
jasmine.clock().install();
|
||||
const baseTime = new Date();
|
||||
|
||||
jasmine.clock().mockDate(baseTime);
|
||||
|
||||
const storageObject = {
|
||||
state: 'running',
|
||||
dateOfLaunchedProcessUtc: baseTime.toISOString(),
|
||||
};
|
||||
|
||||
const spy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
service.setSilentRenewRunning({ configId: 'configId1' });
|
||||
expect(spy).toHaveBeenCalledOnceWith(
|
||||
'storageSilentRenewRunning',
|
||||
JSON.stringify(storageObject),
|
||||
{ configId: 'configId1' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetSilentRenewRunning', () => {
|
||||
it('set resetSilentRenewRunning to empty string when called', () => {
|
||||
const spy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
service.resetSilentRenewRunning({ configId: 'configId1' });
|
||||
expect(spy).toHaveBeenCalledOnceWith('storageSilentRenewRunning', '', {
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
204
src/flows/flows-data.service.ts
Normal file
204
src/flows/flows-data.service.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { OpenIdConfiguration } from '../config/openid-configuration';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { StoragePersistenceService } from '../storage/storage-persistence.service';
|
||||
import { SilentRenewRunning } from './flows.models';
|
||||
import { RandomService } from './random/random.service';
|
||||
|
||||
@Injectable()
|
||||
export class FlowsDataService {
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly storagePersistenceService = inject(
|
||||
StoragePersistenceService
|
||||
);
|
||||
|
||||
private readonly randomService = inject(RandomService);
|
||||
|
||||
createNonce(configuration: OpenIdConfiguration): string {
|
||||
const nonce = this.randomService.createRandom(40, configuration);
|
||||
|
||||
this.loggerService.logDebug(configuration, 'Nonce created. nonce:' + nonce);
|
||||
this.setNonce(nonce, configuration);
|
||||
|
||||
return nonce;
|
||||
}
|
||||
|
||||
setNonce(nonce: string, configuration: OpenIdConfiguration): void {
|
||||
this.storagePersistenceService.write('authNonce', nonce, configuration);
|
||||
}
|
||||
|
||||
getAuthStateControl(configuration: OpenIdConfiguration | null): string {
|
||||
if (!configuration) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.storagePersistenceService.read(
|
||||
'authStateControl',
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
setAuthStateControl(
|
||||
authStateControl: string,
|
||||
configuration: OpenIdConfiguration | null
|
||||
): boolean {
|
||||
if (!configuration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.storagePersistenceService.write(
|
||||
'authStateControl',
|
||||
authStateControl,
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
getExistingOrCreateAuthStateControl(configuration: OpenIdConfiguration): any {
|
||||
let state = this.storagePersistenceService.read(
|
||||
'authStateControl',
|
||||
configuration
|
||||
);
|
||||
|
||||
if (!state) {
|
||||
state = this.randomService.createRandom(40, configuration);
|
||||
this.storagePersistenceService.write(
|
||||
'authStateControl',
|
||||
state,
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
setSessionState(sessionState: any, configuration: OpenIdConfiguration): void {
|
||||
this.storagePersistenceService.write(
|
||||
'session_state',
|
||||
sessionState,
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
resetStorageFlowData(configuration: OpenIdConfiguration): void {
|
||||
this.storagePersistenceService.resetStorageFlowData(configuration);
|
||||
}
|
||||
|
||||
getCodeVerifier(configuration: OpenIdConfiguration): any {
|
||||
return this.storagePersistenceService.read('codeVerifier', configuration);
|
||||
}
|
||||
|
||||
createCodeVerifier(configuration: OpenIdConfiguration): string {
|
||||
const codeVerifier = this.randomService.createRandom(67, configuration);
|
||||
|
||||
this.storagePersistenceService.write(
|
||||
'codeVerifier',
|
||||
codeVerifier,
|
||||
configuration
|
||||
);
|
||||
|
||||
return codeVerifier;
|
||||
}
|
||||
|
||||
isCodeFlowInProgress(configuration: OpenIdConfiguration): boolean {
|
||||
return !!this.storagePersistenceService.read(
|
||||
'storageCodeFlowInProgress',
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
setCodeFlowInProgress(configuration: OpenIdConfiguration): void {
|
||||
this.storagePersistenceService.write(
|
||||
'storageCodeFlowInProgress',
|
||||
true,
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
resetCodeFlowInProgress(configuration: OpenIdConfiguration): void {
|
||||
this.storagePersistenceService.write(
|
||||
'storageCodeFlowInProgress',
|
||||
false,
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
isSilentRenewRunning(configuration: OpenIdConfiguration): boolean {
|
||||
const { configId, silentRenewTimeoutInSeconds } = configuration;
|
||||
const storageObject = this.getSilentRenewRunningStorageEntry(configuration);
|
||||
|
||||
if (!storageObject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (storageObject.state === 'not-running') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const timeOutInMilliseconds = (silentRenewTimeoutInSeconds ?? 0) * 1000;
|
||||
const dateOfLaunchedProcessUtc = Date.parse(
|
||||
storageObject.dateOfLaunchedProcessUtc
|
||||
);
|
||||
const currentDateUtc = Date.parse(new Date().toISOString());
|
||||
const elapsedTimeInMilliseconds = Math.abs(
|
||||
currentDateUtc - dateOfLaunchedProcessUtc
|
||||
);
|
||||
const isProbablyStuck = elapsedTimeInMilliseconds > timeOutInMilliseconds;
|
||||
|
||||
if (isProbablyStuck) {
|
||||
this.loggerService.logDebug(
|
||||
configuration,
|
||||
'silent renew process is probably stuck, state will be reset.',
|
||||
configId
|
||||
);
|
||||
this.resetSilentRenewRunning(configuration);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return storageObject.state === 'running';
|
||||
}
|
||||
|
||||
setSilentRenewRunning(configuration: OpenIdConfiguration): void {
|
||||
const storageObject: SilentRenewRunning = {
|
||||
state: 'running',
|
||||
dateOfLaunchedProcessUtc: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.storagePersistenceService.write(
|
||||
'storageSilentRenewRunning',
|
||||
JSON.stringify(storageObject),
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
resetSilentRenewRunning(configuration: OpenIdConfiguration | null): void {
|
||||
if (!configuration) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.storagePersistenceService.write(
|
||||
'storageSilentRenewRunning',
|
||||
'',
|
||||
configuration
|
||||
);
|
||||
}
|
||||
|
||||
private getSilentRenewRunningStorageEntry(
|
||||
configuration: OpenIdConfiguration
|
||||
): SilentRenewRunning {
|
||||
const storageEntry = this.storagePersistenceService.read(
|
||||
'storageSilentRenewRunning',
|
||||
configuration
|
||||
);
|
||||
|
||||
if (!storageEntry) {
|
||||
return {
|
||||
dateOfLaunchedProcessUtc: '',
|
||||
state: 'not-running',
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.parse(storageEntry);
|
||||
}
|
||||
}
|
||||
6
src/flows/flows.models.ts
Normal file
6
src/flows/flows.models.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface SilentRenewRunning {
|
||||
state: SilentRenewRunningState;
|
||||
dateOfLaunchedProcessUtc: string;
|
||||
}
|
||||
|
||||
export type SilentRenewRunningState = 'running' | 'not-running';
|
||||
226
src/flows/flows.service.spec.ts
Normal file
226
src/flows/flows.service.spec.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { mockProvider } from '../../test/auto-mock';
|
||||
import { CallbackContext } from './callback-context';
|
||||
import { CodeFlowCallbackHandlerService } from './callback-handling/code-flow-callback-handler.service';
|
||||
import { HistoryJwtKeysCallbackHandlerService } from './callback-handling/history-jwt-keys-callback-handler.service';
|
||||
import { ImplicitFlowCallbackHandlerService } from './callback-handling/implicit-flow-callback-handler.service';
|
||||
import { RefreshSessionCallbackHandlerService } from './callback-handling/refresh-session-callback-handler.service';
|
||||
import { RefreshTokenCallbackHandlerService } from './callback-handling/refresh-token-callback-handler.service';
|
||||
import { StateValidationCallbackHandlerService } from './callback-handling/state-validation-callback-handler.service';
|
||||
import { UserCallbackHandlerService } from './callback-handling/user-callback-handler.service';
|
||||
import { FlowsService } from './flows.service';
|
||||
|
||||
describe('Flows Service', () => {
|
||||
let service: FlowsService;
|
||||
let codeFlowCallbackHandlerService: CodeFlowCallbackHandlerService;
|
||||
let implicitFlowCallbackHandlerService: ImplicitFlowCallbackHandlerService;
|
||||
let historyJwtKeysCallbackHandlerService: HistoryJwtKeysCallbackHandlerService;
|
||||
let userCallbackHandlerService: UserCallbackHandlerService;
|
||||
let stateValidationCallbackHandlerService: StateValidationCallbackHandlerService;
|
||||
let refreshSessionCallbackHandlerService: RefreshSessionCallbackHandlerService;
|
||||
let refreshTokenCallbackHandlerService: RefreshTokenCallbackHandlerService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
FlowsService,
|
||||
mockProvider(CodeFlowCallbackHandlerService),
|
||||
mockProvider(ImplicitFlowCallbackHandlerService),
|
||||
mockProvider(HistoryJwtKeysCallbackHandlerService),
|
||||
mockProvider(UserCallbackHandlerService),
|
||||
mockProvider(StateValidationCallbackHandlerService),
|
||||
mockProvider(RefreshSessionCallbackHandlerService),
|
||||
mockProvider(RefreshTokenCallbackHandlerService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(FlowsService);
|
||||
codeFlowCallbackHandlerService = TestBed.inject(
|
||||
CodeFlowCallbackHandlerService
|
||||
);
|
||||
implicitFlowCallbackHandlerService = TestBed.inject(
|
||||
ImplicitFlowCallbackHandlerService
|
||||
);
|
||||
historyJwtKeysCallbackHandlerService = TestBed.inject(
|
||||
HistoryJwtKeysCallbackHandlerService
|
||||
);
|
||||
userCallbackHandlerService = TestBed.inject(UserCallbackHandlerService);
|
||||
stateValidationCallbackHandlerService = TestBed.inject(
|
||||
StateValidationCallbackHandlerService
|
||||
);
|
||||
refreshSessionCallbackHandlerService = TestBed.inject(
|
||||
RefreshSessionCallbackHandlerService
|
||||
);
|
||||
refreshTokenCallbackHandlerService = TestBed.inject(
|
||||
RefreshTokenCallbackHandlerService
|
||||
);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('processCodeFlowCallback', () => {
|
||||
it('calls all methods correctly', waitForAsync(() => {
|
||||
const codeFlowCallbackSpy = spyOn(
|
||||
codeFlowCallbackHandlerService,
|
||||
'codeFlowCallback'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const codeFlowCodeRequestSpy = spyOn(
|
||||
codeFlowCallbackHandlerService,
|
||||
'codeFlowCodeRequest'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackHistoryAndResetJwtKeysSpy = spyOn(
|
||||
historyJwtKeysCallbackHandlerService,
|
||||
'callbackHistoryAndResetJwtKeys'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackStateValidationSpy = spyOn(
|
||||
stateValidationCallbackHandlerService,
|
||||
'callbackStateValidation'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackUserSpy = spyOn(
|
||||
userCallbackHandlerService,
|
||||
'callbackUser'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
},
|
||||
];
|
||||
|
||||
service
|
||||
.processCodeFlowCallback('some-url1234', allConfigs[0], allConfigs)
|
||||
.subscribe((value) => {
|
||||
expect(value).toEqual({} as CallbackContext);
|
||||
expect(codeFlowCallbackSpy).toHaveBeenCalledOnceWith(
|
||||
'some-url1234',
|
||||
allConfigs[0]
|
||||
);
|
||||
expect(codeFlowCodeRequestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalledTimes(1);
|
||||
expect(callbackStateValidationSpy).toHaveBeenCalledTimes(1);
|
||||
expect(callbackUserSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('processSilentRenewCodeFlowCallback', () => {
|
||||
it('calls all methods correctly', waitForAsync(() => {
|
||||
const codeFlowCodeRequestSpy = spyOn(
|
||||
codeFlowCallbackHandlerService,
|
||||
'codeFlowCodeRequest'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackHistoryAndResetJwtKeysSpy = spyOn(
|
||||
historyJwtKeysCallbackHandlerService,
|
||||
'callbackHistoryAndResetJwtKeys'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackStateValidationSpy = spyOn(
|
||||
stateValidationCallbackHandlerService,
|
||||
'callbackStateValidation'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackUserSpy = spyOn(
|
||||
userCallbackHandlerService,
|
||||
'callbackUser'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
},
|
||||
];
|
||||
|
||||
service
|
||||
.processSilentRenewCodeFlowCallback(
|
||||
{} as CallbackContext,
|
||||
allConfigs[0],
|
||||
allConfigs
|
||||
)
|
||||
.subscribe((value) => {
|
||||
expect(value).toEqual({} as CallbackContext);
|
||||
expect(codeFlowCodeRequestSpy).toHaveBeenCalled();
|
||||
expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalled();
|
||||
expect(callbackStateValidationSpy).toHaveBeenCalled();
|
||||
expect(callbackUserSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('processImplicitFlowCallback', () => {
|
||||
it('calls all methods correctly', waitForAsync(() => {
|
||||
const implicitFlowCallbackSpy = spyOn(
|
||||
implicitFlowCallbackHandlerService,
|
||||
'implicitFlowCallback'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackHistoryAndResetJwtKeysSpy = spyOn(
|
||||
historyJwtKeysCallbackHandlerService,
|
||||
'callbackHistoryAndResetJwtKeys'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackStateValidationSpy = spyOn(
|
||||
stateValidationCallbackHandlerService,
|
||||
'callbackStateValidation'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackUserSpy = spyOn(
|
||||
userCallbackHandlerService,
|
||||
'callbackUser'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
},
|
||||
];
|
||||
|
||||
service
|
||||
.processImplicitFlowCallback(allConfigs[0], allConfigs, 'any-hash')
|
||||
.subscribe((value) => {
|
||||
expect(value).toEqual({} as CallbackContext);
|
||||
expect(implicitFlowCallbackSpy).toHaveBeenCalled();
|
||||
expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalled();
|
||||
expect(callbackStateValidationSpy).toHaveBeenCalled();
|
||||
expect(callbackUserSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('processRefreshToken', () => {
|
||||
it('calls all methods correctly', waitForAsync(() => {
|
||||
const refreshSessionWithRefreshTokensSpy = spyOn(
|
||||
refreshSessionCallbackHandlerService,
|
||||
'refreshSessionWithRefreshTokens'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const refreshTokensRequestTokensSpy = spyOn(
|
||||
refreshTokenCallbackHandlerService,
|
||||
'refreshTokensRequestTokens'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackHistoryAndResetJwtKeysSpy = spyOn(
|
||||
historyJwtKeysCallbackHandlerService,
|
||||
'callbackHistoryAndResetJwtKeys'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackStateValidationSpy = spyOn(
|
||||
stateValidationCallbackHandlerService,
|
||||
'callbackStateValidation'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const callbackUserSpy = spyOn(
|
||||
userCallbackHandlerService,
|
||||
'callbackUser'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
},
|
||||
];
|
||||
|
||||
service
|
||||
.processRefreshToken(allConfigs[0], allConfigs)
|
||||
.subscribe((value) => {
|
||||
expect(value).toEqual({} as CallbackContext);
|
||||
expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled();
|
||||
expect(refreshTokensRequestTokensSpy).toHaveBeenCalled();
|
||||
expect(callbackHistoryAndResetJwtKeysSpy).toHaveBeenCalled();
|
||||
expect(callbackStateValidationSpy).toHaveBeenCalled();
|
||||
expect(callbackUserSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
182
src/flows/flows.service.ts
Normal file
182
src/flows/flows.service.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Observable } from 'rxjs';
|
||||
import { concatMap } from 'rxjs/operators';
|
||||
import { OpenIdConfiguration } from '../config/openid-configuration';
|
||||
import { CallbackContext } from './callback-context';
|
||||
import { CodeFlowCallbackHandlerService } from './callback-handling/code-flow-callback-handler.service';
|
||||
import { HistoryJwtKeysCallbackHandlerService } from './callback-handling/history-jwt-keys-callback-handler.service';
|
||||
import { ImplicitFlowCallbackHandlerService } from './callback-handling/implicit-flow-callback-handler.service';
|
||||
import { RefreshSessionCallbackHandlerService } from './callback-handling/refresh-session-callback-handler.service';
|
||||
import { RefreshTokenCallbackHandlerService } from './callback-handling/refresh-token-callback-handler.service';
|
||||
import { StateValidationCallbackHandlerService } from './callback-handling/state-validation-callback-handler.service';
|
||||
import { UserCallbackHandlerService } from './callback-handling/user-callback-handler.service';
|
||||
|
||||
@Injectable()
|
||||
export class FlowsService {
|
||||
private readonly codeFlowCallbackHandlerService = inject(
|
||||
CodeFlowCallbackHandlerService
|
||||
);
|
||||
|
||||
private readonly implicitFlowCallbackHandlerService = inject(
|
||||
ImplicitFlowCallbackHandlerService
|
||||
);
|
||||
|
||||
private readonly historyJwtKeysCallbackHandlerService = inject(
|
||||
HistoryJwtKeysCallbackHandlerService
|
||||
);
|
||||
|
||||
private readonly userHandlerService = inject(UserCallbackHandlerService);
|
||||
|
||||
private readonly stateValidationCallbackHandlerService = inject(
|
||||
StateValidationCallbackHandlerService
|
||||
);
|
||||
|
||||
private readonly refreshSessionCallbackHandlerService = inject(
|
||||
RefreshSessionCallbackHandlerService
|
||||
);
|
||||
|
||||
private readonly refreshTokenCallbackHandlerService = inject(
|
||||
RefreshTokenCallbackHandlerService
|
||||
);
|
||||
|
||||
processCodeFlowCallback(
|
||||
urlToCheck: string,
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[]
|
||||
): Observable<CallbackContext> {
|
||||
return this.codeFlowCallbackHandlerService
|
||||
.codeFlowCallback(urlToCheck, config)
|
||||
.pipe(
|
||||
concatMap((callbackContext) =>
|
||||
this.codeFlowCallbackHandlerService.codeFlowCodeRequest(
|
||||
callbackContext,
|
||||
config
|
||||
)
|
||||
),
|
||||
concatMap((callbackContext) =>
|
||||
this.historyJwtKeysCallbackHandlerService.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
),
|
||||
concatMap((callbackContext) =>
|
||||
this.stateValidationCallbackHandlerService.callbackStateValidation(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
),
|
||||
concatMap((callbackContext) =>
|
||||
this.userHandlerService.callbackUser(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
processSilentRenewCodeFlowCallback(
|
||||
firstContext: CallbackContext,
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[]
|
||||
): Observable<CallbackContext> {
|
||||
return this.codeFlowCallbackHandlerService
|
||||
.codeFlowCodeRequest(firstContext, config)
|
||||
.pipe(
|
||||
concatMap((callbackContext) =>
|
||||
this.historyJwtKeysCallbackHandlerService.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
),
|
||||
concatMap((callbackContext) =>
|
||||
this.stateValidationCallbackHandlerService.callbackStateValidation(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
),
|
||||
concatMap((callbackContext) =>
|
||||
this.userHandlerService.callbackUser(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
processImplicitFlowCallback(
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[],
|
||||
hash?: string
|
||||
): Observable<CallbackContext> {
|
||||
return this.implicitFlowCallbackHandlerService
|
||||
.implicitFlowCallback(config, allConfigs, hash)
|
||||
.pipe(
|
||||
concatMap((callbackContext) =>
|
||||
this.historyJwtKeysCallbackHandlerService.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
),
|
||||
concatMap((callbackContext) =>
|
||||
this.stateValidationCallbackHandlerService.callbackStateValidation(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
),
|
||||
concatMap((callbackContext) =>
|
||||
this.userHandlerService.callbackUser(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
processRefreshToken(
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[],
|
||||
customParamsRefresh?: { [key: string]: string | number | boolean }
|
||||
): Observable<CallbackContext> {
|
||||
return this.refreshSessionCallbackHandlerService
|
||||
.refreshSessionWithRefreshTokens(config)
|
||||
.pipe(
|
||||
concatMap((callbackContext) =>
|
||||
this.refreshTokenCallbackHandlerService.refreshTokensRequestTokens(
|
||||
callbackContext,
|
||||
config,
|
||||
customParamsRefresh
|
||||
)
|
||||
),
|
||||
concatMap((callbackContext) =>
|
||||
this.historyJwtKeysCallbackHandlerService.callbackHistoryAndResetJwtKeys(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
),
|
||||
concatMap((callbackContext) =>
|
||||
this.stateValidationCallbackHandlerService.callbackStateValidation(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
),
|
||||
concatMap((callbackContext) =>
|
||||
this.userHandlerService.callbackUser(
|
||||
callbackContext,
|
||||
config,
|
||||
allConfigs
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
64
src/flows/random/random.service.spec.ts
Normal file
64
src/flows/random/random.service.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { mockProvider } from '../../../test/auto-mock';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { CryptoService } from '../../utils/crypto/crypto.service';
|
||||
import { RandomService } from './random.service';
|
||||
|
||||
describe('RandomService Tests', () => {
|
||||
let randomService: RandomService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [RandomService, mockProvider(LoggerService), CryptoService],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
randomService = TestBed.inject(RandomService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(randomService).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be not equal', () => {
|
||||
const r1 = randomService.createRandom(45, { configId: 'configId1' });
|
||||
const r2 = randomService.createRandom(45, { configId: 'configId1' });
|
||||
|
||||
expect(r1).not.toEqual(r2);
|
||||
});
|
||||
|
||||
it('correct length with high number', () => {
|
||||
const r1 = randomService.createRandom(79, { configId: 'configId1' });
|
||||
const result = r1.length;
|
||||
|
||||
expect(result).toBe(79);
|
||||
});
|
||||
|
||||
it('correct length with small number', () => {
|
||||
const r1 = randomService.createRandom(7, { configId: 'configId1' });
|
||||
const result = r1.length;
|
||||
|
||||
expect(result).toBe(7);
|
||||
});
|
||||
|
||||
it('correct length with 0', () => {
|
||||
const r1 = randomService.createRandom(0, { configId: 'configId1' });
|
||||
const result = r1.length;
|
||||
|
||||
expect(result).toBe(0);
|
||||
expect(r1).toBe('');
|
||||
});
|
||||
|
||||
for (let index = 1; index < 7; index++) {
|
||||
it('Giving back 10 or more characters when called with numbers less than 7', () => {
|
||||
const requiredLengthSmallerThenSeven = index;
|
||||
const fallbackLength = 10;
|
||||
const r1 = randomService.createRandom(requiredLengthSmallerThenSeven, {
|
||||
configId: 'configId1',
|
||||
});
|
||||
|
||||
expect(r1.length).toBeGreaterThanOrEqual(fallbackLength);
|
||||
});
|
||||
}
|
||||
});
|
||||
60
src/flows/random/random.service.ts
Normal file
60
src/flows/random/random.service.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { OpenIdConfiguration } from '../../config/openid-configuration';
|
||||
import { LoggerService } from '../../logging/logger.service';
|
||||
import { CryptoService } from '../../utils/crypto/crypto.service';
|
||||
|
||||
@Injectable()
|
||||
export class RandomService {
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly cryptoService = inject(CryptoService);
|
||||
|
||||
createRandom(
|
||||
requiredLength: number,
|
||||
configuration: OpenIdConfiguration
|
||||
): string {
|
||||
if (requiredLength <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (requiredLength > 0 && requiredLength < 7) {
|
||||
this.loggerService.logWarning(
|
||||
configuration,
|
||||
`RandomService called with ${requiredLength} but 7 chars is the minimum, returning 10 chars`
|
||||
);
|
||||
requiredLength = 10;
|
||||
}
|
||||
|
||||
const length = requiredLength - 6;
|
||||
const arr = new Uint8Array(Math.floor(length / 2));
|
||||
const crypto = this.cryptoService.getCrypto();
|
||||
|
||||
if (crypto) {
|
||||
crypto.getRandomValues(arr);
|
||||
}
|
||||
|
||||
return Array.from(arr, this.toHex).join('') + this.randomString(7);
|
||||
}
|
||||
|
||||
private toHex(dec: number): string {
|
||||
return ('0' + dec.toString(16)).substr(-2);
|
||||
}
|
||||
|
||||
private randomString(length: number): string {
|
||||
let result = '';
|
||||
const characters =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
const values = new Uint32Array(length);
|
||||
const crypto = this.cryptoService.getCrypto();
|
||||
|
||||
if (crypto) {
|
||||
crypto.getRandomValues(values);
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters[values[i] % characters.length];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
75
src/flows/reset-auth-data.service.spec.ts
Normal file
75
src/flows/reset-auth-data.service.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { mockProvider } from '../../test/auto-mock';
|
||||
import { AuthStateService } from '../auth-state/auth-state.service';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { UserService } from '../user-data/user.service';
|
||||
import { FlowsDataService } from './flows-data.service';
|
||||
import { ResetAuthDataService } from './reset-auth-data.service';
|
||||
|
||||
describe('ResetAuthDataService', () => {
|
||||
let service: ResetAuthDataService;
|
||||
let userService: UserService;
|
||||
let flowsDataService: FlowsDataService;
|
||||
let authStateService: AuthStateService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ResetAuthDataService,
|
||||
mockProvider(AuthStateService),
|
||||
mockProvider(FlowsDataService),
|
||||
mockProvider(UserService),
|
||||
mockProvider(LoggerService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(ResetAuthDataService);
|
||||
userService = TestBed.inject(UserService);
|
||||
flowsDataService = TestBed.inject(FlowsDataService);
|
||||
authStateService = TestBed.inject(AuthStateService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('resetAuthorizationData', () => {
|
||||
it('calls resetUserDataInStore when autoUserInfo is true', () => {
|
||||
const resetUserDataInStoreSpy = spyOn(
|
||||
userService,
|
||||
'resetUserDataInStore'
|
||||
);
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
},
|
||||
];
|
||||
|
||||
service.resetAuthorizationData(allConfigs[0], allConfigs);
|
||||
expect(resetUserDataInStoreSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls correct methods', () => {
|
||||
const resetStorageFlowDataSpy = spyOn(
|
||||
flowsDataService,
|
||||
'resetStorageFlowData'
|
||||
);
|
||||
const setUnauthorizedAndFireEventSpy = spyOn(
|
||||
authStateService,
|
||||
'setUnauthenticatedAndFireEvent'
|
||||
);
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
},
|
||||
];
|
||||
|
||||
service.resetAuthorizationData(allConfigs[0], allConfigs);
|
||||
|
||||
expect(resetStorageFlowDataSpy).toHaveBeenCalled();
|
||||
expect(setUnauthorizedAndFireEventSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
38
src/flows/reset-auth-data.service.ts
Normal file
38
src/flows/reset-auth-data.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { AuthStateService } from '../auth-state/auth-state.service';
|
||||
import { OpenIdConfiguration } from '../config/openid-configuration';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { UserService } from '../user-data/user.service';
|
||||
import { FlowsDataService } from './flows-data.service';
|
||||
|
||||
@Injectable()
|
||||
export class ResetAuthDataService {
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly userService = inject(UserService);
|
||||
|
||||
private readonly flowsDataService = inject(FlowsDataService);
|
||||
|
||||
private readonly authStateService = inject(AuthStateService);
|
||||
|
||||
resetAuthorizationData(
|
||||
currentConfiguration: OpenIdConfiguration | null,
|
||||
allConfigs: OpenIdConfiguration[]
|
||||
): void {
|
||||
if (!currentConfiguration) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.userService.resetUserDataInStore(currentConfiguration, allConfigs);
|
||||
this.flowsDataService.resetStorageFlowData(currentConfiguration);
|
||||
this.authStateService.setUnauthenticatedAndFireEvent(
|
||||
currentConfiguration,
|
||||
allConfigs
|
||||
);
|
||||
|
||||
this.loggerService.logDebug(
|
||||
currentConfiguration,
|
||||
'Local Login information cleaned up and event fired'
|
||||
);
|
||||
}
|
||||
}
|
||||
220
src/flows/signin-key-data.service.spec.ts
Normal file
220
src/flows/signin-key-data.service.spec.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { HttpResponse } from '@angular/common/http';
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { isObservable, of, throwError } from 'rxjs';
|
||||
import { mockProvider } from '../../test/auto-mock';
|
||||
import { createRetriableStream } from '../../test/create-retriable-stream.helper';
|
||||
import { DataService } from '../api/data.service';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { StoragePersistenceService } from '../storage/storage-persistence.service';
|
||||
import { SigninKeyDataService } from './signin-key-data.service';
|
||||
|
||||
const DUMMY_JWKS = {
|
||||
keys: [
|
||||
{
|
||||
kid: 'random-id',
|
||||
kty: 'RSA',
|
||||
alg: 'RS256',
|
||||
use: 'sig',
|
||||
n: 'some-value',
|
||||
e: 'AQAB',
|
||||
x5c: ['some-value'],
|
||||
x5t: 'some-value',
|
||||
'x5t#S256': 'some-value',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Signin Key Data Service', () => {
|
||||
let service: SigninKeyDataService;
|
||||
let storagePersistenceService: StoragePersistenceService;
|
||||
let dataService: DataService;
|
||||
let loggerService: LoggerService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
SigninKeyDataService,
|
||||
mockProvider(DataService),
|
||||
mockProvider(LoggerService),
|
||||
mockProvider(StoragePersistenceService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(SigninKeyDataService);
|
||||
storagePersistenceService = TestBed.inject(StoragePersistenceService);
|
||||
dataService = TestBed.inject(DataService);
|
||||
loggerService = TestBed.inject(LoggerService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('getSigningKeys', () => {
|
||||
it('throws error when no wellKnownEndpoints given', waitForAsync(() => {
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', { configId: 'configId1' })
|
||||
.and.returnValue(null);
|
||||
const result = service.getSigningKeys({ configId: 'configId1' });
|
||||
|
||||
result.subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('throws error when no jwksUri given', waitForAsync(() => {
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', { configId: 'configId1' })
|
||||
.and.returnValue({ jwksUri: null });
|
||||
const result = service.getSigningKeys({ configId: 'configId1' });
|
||||
|
||||
result.subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls dataservice if jwksurl is given', waitForAsync(() => {
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', { configId: 'configId1' })
|
||||
.and.returnValue({ jwksUri: 'someUrl' });
|
||||
const spy = spyOn(dataService, 'get').and.callFake(() => of());
|
||||
|
||||
const result = service.getSigningKeys({ configId: 'configId1' });
|
||||
|
||||
result.subscribe({
|
||||
complete: () => {
|
||||
expect(spy).toHaveBeenCalledOnceWith('someUrl', {
|
||||
configId: 'configId1',
|
||||
});
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('should retry once', waitForAsync(() => {
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', { configId: 'configId1' })
|
||||
.and.returnValue({ jwksUri: 'someUrl' });
|
||||
spyOn(dataService, 'get').and.returnValue(
|
||||
createRetriableStream(
|
||||
throwError(() => new Error('Error')),
|
||||
of(DUMMY_JWKS)
|
||||
)
|
||||
);
|
||||
|
||||
service.getSigningKeys({ configId: 'configId1' }).subscribe({
|
||||
next: (res) => {
|
||||
expect(res).toBeTruthy();
|
||||
expect(res).toEqual(DUMMY_JWKS);
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('should retry twice', waitForAsync(() => {
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', { configId: 'configId1' })
|
||||
.and.returnValue({ jwksUri: 'someUrl' });
|
||||
spyOn(dataService, 'get').and.returnValue(
|
||||
createRetriableStream(
|
||||
throwError(() => new Error('Error')),
|
||||
throwError(() => new Error('Error')),
|
||||
of(DUMMY_JWKS)
|
||||
)
|
||||
);
|
||||
|
||||
service.getSigningKeys({ configId: 'configId1' }).subscribe({
|
||||
next: (res) => {
|
||||
expect(res).toBeTruthy();
|
||||
expect(res).toEqual(DUMMY_JWKS);
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('should fail after three tries', waitForAsync(() => {
|
||||
spyOn(storagePersistenceService, 'read')
|
||||
.withArgs('authWellKnownEndPoints', { configId: 'configId1' })
|
||||
.and.returnValue({ jwksUri: 'someUrl' });
|
||||
spyOn(dataService, 'get').and.returnValue(
|
||||
createRetriableStream(
|
||||
throwError(() => new Error('Error')),
|
||||
throwError(() => new Error('Error')),
|
||||
throwError(() => new Error('Error')),
|
||||
of(DUMMY_JWKS)
|
||||
)
|
||||
);
|
||||
|
||||
service.getSigningKeys({ configId: 'configId1' }).subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('handleErrorGetSigningKeys', () => {
|
||||
it('keeps observable if error is catched', waitForAsync(() => {
|
||||
const result = (service as any).handleErrorGetSigningKeys(
|
||||
new HttpResponse()
|
||||
);
|
||||
const hasTypeObservable = isObservable(result);
|
||||
|
||||
expect(hasTypeObservable).toBeTrue();
|
||||
}));
|
||||
|
||||
it('logs error if error is response', waitForAsync(() => {
|
||||
const logSpy = spyOn(loggerService, 'logError');
|
||||
|
||||
(service as any)
|
||||
.handleErrorGetSigningKeys(
|
||||
new HttpResponse({ status: 400, statusText: 'nono' }),
|
||||
{ configId: 'configId1' }
|
||||
)
|
||||
.subscribe({
|
||||
error: () => {
|
||||
expect(logSpy).toHaveBeenCalledOnceWith(
|
||||
{ configId: 'configId1' },
|
||||
'400 - nono {}'
|
||||
);
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('logs error if error is not a response', waitForAsync(() => {
|
||||
const logSpy = spyOn(loggerService, 'logError');
|
||||
|
||||
(service as any)
|
||||
.handleErrorGetSigningKeys('Just some Error', { configId: 'configId1' })
|
||||
.subscribe({
|
||||
error: () => {
|
||||
expect(logSpy).toHaveBeenCalledOnceWith(
|
||||
{ configId: 'configId1' },
|
||||
'Just some Error'
|
||||
);
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('logs error if error with message property is not a response', waitForAsync(() => {
|
||||
const logSpy = spyOn(loggerService, 'logError');
|
||||
|
||||
(service as any)
|
||||
.handleErrorGetSigningKeys(
|
||||
{ message: 'Just some Error' },
|
||||
{ configId: 'configId1' }
|
||||
)
|
||||
.subscribe({
|
||||
error: () => {
|
||||
expect(logSpy).toHaveBeenCalledOnceWith(
|
||||
{ configId: 'configId1' },
|
||||
'Just some Error'
|
||||
);
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
71
src/flows/signin-key-data.service.ts
Normal file
71
src/flows/signin-key-data.service.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { HttpResponse } from '@ngify/http';
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, retry } from 'rxjs/operators';
|
||||
import { DataService } from '../api/data.service';
|
||||
import { OpenIdConfiguration } from '../config/openid-configuration';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { StoragePersistenceService } from '../storage/storage-persistence.service';
|
||||
import { JwtKeys } from '../validation/jwtkeys';
|
||||
|
||||
@Injectable()
|
||||
export class SigninKeyDataService {
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly storagePersistenceService = inject(
|
||||
StoragePersistenceService
|
||||
);
|
||||
|
||||
private readonly dataService = inject(DataService);
|
||||
|
||||
getSigningKeys(
|
||||
currentConfiguration: OpenIdConfiguration
|
||||
): Observable<JwtKeys> {
|
||||
const authWellKnownEndPoints = this.storagePersistenceService.read(
|
||||
'authWellKnownEndPoints',
|
||||
currentConfiguration
|
||||
);
|
||||
const jwksUri = authWellKnownEndPoints?.jwksUri;
|
||||
|
||||
if (!jwksUri) {
|
||||
const error = `getSigningKeys: authWellKnownEndpoints.jwksUri is: '${jwksUri}'`;
|
||||
|
||||
this.loggerService.logWarning(currentConfiguration, error);
|
||||
|
||||
return throwError(() => new Error(error));
|
||||
}
|
||||
|
||||
this.loggerService.logDebug(
|
||||
currentConfiguration,
|
||||
'Getting signinkeys from ',
|
||||
jwksUri
|
||||
);
|
||||
|
||||
return this.dataService.get<JwtKeys>(jwksUri, currentConfiguration).pipe(
|
||||
retry(2),
|
||||
catchError((e) => this.handleErrorGetSigningKeys(e, currentConfiguration))
|
||||
);
|
||||
}
|
||||
|
||||
private handleErrorGetSigningKeys(
|
||||
errorResponse: HttpResponse<any> | any,
|
||||
currentConfiguration: OpenIdConfiguration
|
||||
): Observable<never> {
|
||||
let errMsg = '';
|
||||
|
||||
if (errorResponse instanceof HttpResponse) {
|
||||
const body = errorResponse.body || {};
|
||||
const err = JSON.stringify(body);
|
||||
const { status, statusText } = errorResponse;
|
||||
|
||||
errMsg = `${status || ''} - ${statusText || ''} ${err || ''}`;
|
||||
} else {
|
||||
const { message } = errorResponse;
|
||||
|
||||
errMsg = !!message ? message : `${errorResponse}`;
|
||||
}
|
||||
this.loggerService.logError(currentConfiguration, errMsg);
|
||||
|
||||
return throwError(() => new Error(errMsg));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user