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:
144
src/callback/callback.service.spec.ts
Normal file
144
src/callback/callback.service.spec.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { mockProvider } from '../../test/auto-mock';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
|
||||
import { UrlService } from '../utils/url/url.service';
|
||||
import { CallbackService } from './callback.service';
|
||||
import { CodeFlowCallbackService } from './code-flow-callback.service';
|
||||
import { ImplicitFlowCallbackService } from './implicit-flow-callback.service';
|
||||
|
||||
describe('CallbackService ', () => {
|
||||
let callbackService: CallbackService;
|
||||
let implicitFlowCallbackService: ImplicitFlowCallbackService;
|
||||
let codeFlowCallbackService: CodeFlowCallbackService;
|
||||
let flowHelper: FlowHelper;
|
||||
let urlService: UrlService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
CallbackService,
|
||||
mockProvider(UrlService),
|
||||
FlowHelper,
|
||||
mockProvider(ImplicitFlowCallbackService),
|
||||
mockProvider(CodeFlowCallbackService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
callbackService = TestBed.inject(CallbackService);
|
||||
flowHelper = TestBed.inject(FlowHelper);
|
||||
implicitFlowCallbackService = TestBed.inject(ImplicitFlowCallbackService);
|
||||
codeFlowCallbackService = TestBed.inject(CodeFlowCallbackService);
|
||||
urlService = TestBed.inject(UrlService);
|
||||
});
|
||||
|
||||
describe('isCallback', () => {
|
||||
it('calls urlService.isCallbackFromSts with passed url', () => {
|
||||
const urlServiceSpy = spyOn(urlService, 'isCallbackFromSts');
|
||||
|
||||
callbackService.isCallback('anyUrl');
|
||||
expect(urlServiceSpy).toHaveBeenCalledOnceWith('anyUrl', undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stsCallback$', () => {
|
||||
it('is of type Observable', () => {
|
||||
expect(callbackService.stsCallback$).toBeInstanceOf(Observable);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallbackAndFireEvents', () => {
|
||||
it('calls authorizedCallbackWithCode if current flow is code flow', waitForAsync(() => {
|
||||
spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true);
|
||||
const authorizedCallbackWithCodeSpy = spyOn(
|
||||
codeFlowCallbackService,
|
||||
'authenticatedCallbackWithCode'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
|
||||
callbackService
|
||||
.handleCallbackAndFireEvents('anyUrl', { configId: 'configId1' }, [
|
||||
{ configId: 'configId1' },
|
||||
])
|
||||
.subscribe(() => {
|
||||
expect(authorizedCallbackWithCodeSpy).toHaveBeenCalledOnceWith(
|
||||
'anyUrl',
|
||||
{ configId: 'configId1' },
|
||||
[{ configId: 'configId1' }]
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls authorizedImplicitFlowCallback without hash if current flow is implicit flow and callbackurl does not include a hash', waitForAsync(() => {
|
||||
spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(false);
|
||||
spyOn(flowHelper, 'isCurrentFlowAnyImplicitFlow').and.returnValue(true);
|
||||
const authorizedCallbackWithCodeSpy = spyOn(
|
||||
implicitFlowCallbackService,
|
||||
'authenticatedImplicitFlowCallback'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
|
||||
callbackService
|
||||
.handleCallbackAndFireEvents('anyUrl', { configId: 'configId1' }, [
|
||||
{ configId: 'configId1' },
|
||||
])
|
||||
.subscribe(() => {
|
||||
expect(authorizedCallbackWithCodeSpy).toHaveBeenCalledWith(
|
||||
{ configId: 'configId1' },
|
||||
[{ configId: 'configId1' }]
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls authorizedImplicitFlowCallback with hash if current flow is implicit flow and callbackurl does include a hash', waitForAsync(() => {
|
||||
spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(false);
|
||||
spyOn(flowHelper, 'isCurrentFlowAnyImplicitFlow').and.returnValue(true);
|
||||
const authorizedCallbackWithCodeSpy = spyOn(
|
||||
implicitFlowCallbackService,
|
||||
'authenticatedImplicitFlowCallback'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
|
||||
callbackService
|
||||
.handleCallbackAndFireEvents(
|
||||
'anyUrlWithAHash#some-string',
|
||||
{ configId: 'configId1' },
|
||||
[{ configId: 'configId1' }]
|
||||
)
|
||||
.subscribe(() => {
|
||||
expect(authorizedCallbackWithCodeSpy).toHaveBeenCalledWith(
|
||||
{ configId: 'configId1' },
|
||||
[{ configId: 'configId1' }],
|
||||
'some-string'
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
it('emits callbackinternal no matter which flow it is', waitForAsync(() => {
|
||||
const callbackSpy = spyOn(
|
||||
(callbackService as any).stsCallbackInternal$,
|
||||
'next'
|
||||
);
|
||||
|
||||
spyOn(flowHelper, 'isCurrentFlowCodeFlow').and.returnValue(true);
|
||||
const authenticatedCallbackWithCodeSpy = spyOn(
|
||||
codeFlowCallbackService,
|
||||
'authenticatedCallbackWithCode'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
|
||||
callbackService
|
||||
.handleCallbackAndFireEvents('anyUrl', { configId: 'configId1' }, [
|
||||
{ configId: 'configId1' },
|
||||
])
|
||||
.subscribe(() => {
|
||||
expect(authenticatedCallbackWithCodeSpy).toHaveBeenCalledOnceWith(
|
||||
'anyUrl',
|
||||
{ configId: 'configId1' },
|
||||
[{ configId: 'configId1' }]
|
||||
);
|
||||
expect(callbackSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
73
src/callback/callback.service.ts
Normal file
73
src/callback/callback.service.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { OpenIdConfiguration } from '../config/openid-configuration';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
|
||||
import { UrlService } from '../utils/url/url.service';
|
||||
import { CodeFlowCallbackService } from './code-flow-callback.service';
|
||||
import { ImplicitFlowCallbackService } from './implicit-flow-callback.service';
|
||||
|
||||
@Injectable()
|
||||
export class CallbackService {
|
||||
private readonly urlService = inject(UrlService);
|
||||
|
||||
private readonly flowHelper = inject(FlowHelper);
|
||||
|
||||
private readonly implicitFlowCallbackService = inject(
|
||||
ImplicitFlowCallbackService
|
||||
);
|
||||
|
||||
private readonly codeFlowCallbackService = inject(CodeFlowCallbackService);
|
||||
|
||||
private readonly stsCallbackInternal$ = new Subject<void>();
|
||||
|
||||
get stsCallback$(): Observable<void> {
|
||||
return this.stsCallbackInternal$.asObservable();
|
||||
}
|
||||
|
||||
isCallback(currentUrl: string, config?: OpenIdConfiguration): boolean {
|
||||
if (!currentUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.urlService.isCallbackFromSts(currentUrl, config);
|
||||
}
|
||||
|
||||
handleCallbackAndFireEvents(
|
||||
currentCallbackUrl: string,
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[]
|
||||
): Observable<CallbackContext> {
|
||||
let callback$: Observable<CallbackContext> = new Observable();
|
||||
|
||||
if (this.flowHelper.isCurrentFlowCodeFlow(config)) {
|
||||
callback$ = this.codeFlowCallbackService.authenticatedCallbackWithCode(
|
||||
currentCallbackUrl,
|
||||
config,
|
||||
allConfigs
|
||||
);
|
||||
} else if (this.flowHelper.isCurrentFlowAnyImplicitFlow(config)) {
|
||||
if (currentCallbackUrl?.includes('#')) {
|
||||
const hash = currentCallbackUrl.substring(
|
||||
currentCallbackUrl.indexOf('#') + 1
|
||||
);
|
||||
|
||||
callback$ =
|
||||
this.implicitFlowCallbackService.authenticatedImplicitFlowCallback(
|
||||
config,
|
||||
allConfigs,
|
||||
hash
|
||||
);
|
||||
} else {
|
||||
callback$ =
|
||||
this.implicitFlowCallbackService.authenticatedImplicitFlowCallback(
|
||||
config,
|
||||
allConfigs
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return callback$.pipe(tap(() => this.stsCallbackInternal$.next()));
|
||||
}
|
||||
}
|
||||
198
src/callback/code-flow-callback.service.spec.ts
Normal file
198
src/callback/code-flow-callback.service.spec.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { mockProvider } from '../../test/auto-mock';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowsDataService } from '../flows/flows-data.service';
|
||||
import { FlowsService } from '../flows/flows.service';
|
||||
import { CodeFlowCallbackService } from './code-flow-callback.service';
|
||||
import { IntervalService } from './interval.service';
|
||||
|
||||
describe('CodeFlowCallbackService ', () => {
|
||||
let codeFlowCallbackService: CodeFlowCallbackService;
|
||||
let intervalService: IntervalService;
|
||||
let flowsService: FlowsService;
|
||||
let flowsDataService: FlowsDataService;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule],
|
||||
providers: [
|
||||
CodeFlowCallbackService,
|
||||
mockProvider(FlowsService),
|
||||
mockProvider(FlowsDataService),
|
||||
mockProvider(IntervalService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
codeFlowCallbackService = TestBed.inject(CodeFlowCallbackService);
|
||||
intervalService = TestBed.inject(IntervalService);
|
||||
flowsDataService = TestBed.inject(FlowsDataService);
|
||||
flowsService = TestBed.inject(FlowsService);
|
||||
router = TestBed.inject(Router);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(codeFlowCallbackService).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('authenticatedCallbackWithCode', () => {
|
||||
it('calls flowsService.processCodeFlowCallback with correct url', () => {
|
||||
const spy = spyOn(
|
||||
flowsService,
|
||||
'processCodeFlowCallback'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
//spyOn(configurationProvider, 'getOpenIDConfiguration').and.returnValue({ triggerAuthorizationResultEvent: true });
|
||||
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
triggerAuthorizationResultEvent: true,
|
||||
};
|
||||
|
||||
codeFlowCallbackService.authenticatedCallbackWithCode(
|
||||
'some-url1',
|
||||
config,
|
||||
[config]
|
||||
);
|
||||
expect(spy).toHaveBeenCalledOnceWith('some-url1', config, [config]);
|
||||
});
|
||||
|
||||
it('does only call resetCodeFlowInProgress if triggerAuthorizationResultEvent is true and isRenewProcess is true', waitForAsync(() => {
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: '',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: null,
|
||||
isRenewProcess: true,
|
||||
jwtKeys: null,
|
||||
validationResult: null,
|
||||
existingIdToken: '',
|
||||
};
|
||||
const spy = spyOn(
|
||||
flowsService,
|
||||
'processCodeFlowCallback'
|
||||
).and.returnValue(of(callbackContext));
|
||||
const flowsDataSpy = spyOn(flowsDataService, 'resetCodeFlowInProgress');
|
||||
const routerSpy = spyOn(router, 'navigateByUrl');
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
triggerAuthorizationResultEvent: true,
|
||||
};
|
||||
|
||||
codeFlowCallbackService
|
||||
.authenticatedCallbackWithCode('some-url2', config, [config])
|
||||
.subscribe(() => {
|
||||
expect(spy).toHaveBeenCalledOnceWith('some-url2', config, [config]);
|
||||
expect(routerSpy).not.toHaveBeenCalled();
|
||||
expect(flowsDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls router and resetCodeFlowInProgress if triggerAuthorizationResultEvent is false and isRenewProcess is false', waitForAsync(() => {
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: '',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: null,
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: null,
|
||||
existingIdToken: '',
|
||||
};
|
||||
const spy = spyOn(
|
||||
flowsService,
|
||||
'processCodeFlowCallback'
|
||||
).and.returnValue(of(callbackContext));
|
||||
const flowsDataSpy = spyOn(flowsDataService, 'resetCodeFlowInProgress');
|
||||
const routerSpy = spyOn(router, 'navigateByUrl');
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
triggerAuthorizationResultEvent: false,
|
||||
postLoginRoute: 'postLoginRoute',
|
||||
};
|
||||
|
||||
codeFlowCallbackService
|
||||
.authenticatedCallbackWithCode('some-url3', config, [config])
|
||||
.subscribe(() => {
|
||||
expect(spy).toHaveBeenCalledOnceWith('some-url3', config, [config]);
|
||||
expect(routerSpy).toHaveBeenCalledOnceWith('postLoginRoute');
|
||||
expect(flowsDataSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('resetSilentRenewRunning, resetCodeFlowInProgress and stopPeriodicallTokenCheck in case of error', waitForAsync(() => {
|
||||
spyOn(flowsService, 'processCodeFlowCallback').and.returnValue(
|
||||
throwError(() => new Error('error'))
|
||||
);
|
||||
const resetSilentRenewRunningSpy = spyOn(
|
||||
flowsDataService,
|
||||
'resetSilentRenewRunning'
|
||||
);
|
||||
const resetCodeFlowInProgressSpy = spyOn(
|
||||
flowsDataService,
|
||||
'resetCodeFlowInProgress'
|
||||
);
|
||||
const stopPeriodicallTokenCheckSpy = spyOn(
|
||||
intervalService,
|
||||
'stopPeriodicTokenCheck'
|
||||
);
|
||||
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
triggerAuthorizationResultEvent: false,
|
||||
postLoginRoute: 'postLoginRoute',
|
||||
};
|
||||
|
||||
codeFlowCallbackService
|
||||
.authenticatedCallbackWithCode('some-url4', config, [config])
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
|
||||
expect(resetCodeFlowInProgressSpy).toHaveBeenCalled();
|
||||
expect(stopPeriodicallTokenCheckSpy).toHaveBeenCalled();
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it(`navigates to unauthorizedRoute in case of error and in case of error and
|
||||
triggerAuthorizationResultEvent is false`, waitForAsync(() => {
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
|
||||
spyOn(flowsService, 'processCodeFlowCallback').and.returnValue(
|
||||
throwError(() => new Error('error'))
|
||||
);
|
||||
const resetSilentRenewRunningSpy = spyOn(
|
||||
flowsDataService,
|
||||
'resetSilentRenewRunning'
|
||||
);
|
||||
const stopPeriodicallTokenCheckSpy = spyOn(
|
||||
intervalService,
|
||||
'stopPeriodicTokenCheck'
|
||||
);
|
||||
const routerSpy = spyOn(router, 'navigateByUrl');
|
||||
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
triggerAuthorizationResultEvent: false,
|
||||
unauthorizedRoute: 'unauthorizedRoute',
|
||||
};
|
||||
|
||||
codeFlowCallbackService
|
||||
.authenticatedCallbackWithCode('some-url5', config, [config])
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
|
||||
expect(stopPeriodicallTokenCheckSpy).toHaveBeenCalled();
|
||||
expect(err).toBeTruthy();
|
||||
expect(routerSpy).toHaveBeenCalledOnceWith('unauthorizedRoute');
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
56
src/callback/code-flow-callback.service.ts
Normal file
56
src/callback/code-flow-callback.service.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
import { OpenIdConfiguration } from '../config/openid-configuration';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowsDataService } from '../flows/flows-data.service';
|
||||
import { FlowsService } from '../flows/flows.service';
|
||||
import { IntervalService } from './interval.service';
|
||||
|
||||
@Injectable()
|
||||
export class CodeFlowCallbackService {
|
||||
private readonly flowsService = inject(FlowsService);
|
||||
|
||||
private readonly router = inject(Router);
|
||||
|
||||
private readonly flowsDataService = inject(FlowsDataService);
|
||||
|
||||
private readonly intervalService = inject(IntervalService);
|
||||
|
||||
authenticatedCallbackWithCode(
|
||||
urlToCheck: string,
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[]
|
||||
): Observable<CallbackContext> {
|
||||
const isRenewProcess = this.flowsDataService.isSilentRenewRunning(config);
|
||||
const { triggerAuthorizationResultEvent } = config;
|
||||
|
||||
const postLoginRoute = config.postLoginRoute || '/';
|
||||
const unauthorizedRoute = config.unauthorizedRoute || '/';
|
||||
|
||||
return this.flowsService
|
||||
.processCodeFlowCallback(urlToCheck, config, allConfigs)
|
||||
.pipe(
|
||||
tap((callbackContext) => {
|
||||
this.flowsDataService.resetCodeFlowInProgress(config);
|
||||
if (
|
||||
!triggerAuthorizationResultEvent &&
|
||||
!callbackContext.isRenewProcess
|
||||
) {
|
||||
this.router.navigateByUrl(postLoginRoute);
|
||||
}
|
||||
}),
|
||||
catchError((error) => {
|
||||
this.flowsDataService.resetSilentRenewRunning(config);
|
||||
this.flowsDataService.resetCodeFlowInProgress(config);
|
||||
this.intervalService.stopPeriodicTokenCheck();
|
||||
if (!triggerAuthorizationResultEvent && !isRenewProcess) {
|
||||
this.router.navigateByUrl(unauthorizedRoute);
|
||||
}
|
||||
|
||||
return throwError(() => new Error(error));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
185
src/callback/implicit-flow-callback.service.spec.ts
Normal file
185
src/callback/implicit-flow-callback.service.spec.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { mockProvider } from '../../test/auto-mock';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowsDataService } from '../flows/flows-data.service';
|
||||
import { FlowsService } from '../flows/flows.service';
|
||||
import { ImplicitFlowCallbackService } from './implicit-flow-callback.service';
|
||||
import { IntervalService } from './interval.service';
|
||||
|
||||
describe('ImplicitFlowCallbackService ', () => {
|
||||
let implicitFlowCallbackService: ImplicitFlowCallbackService;
|
||||
let intervalService: IntervalService;
|
||||
let flowsService: FlowsService;
|
||||
let flowsDataService: FlowsDataService;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule],
|
||||
providers: [
|
||||
mockProvider(FlowsService),
|
||||
mockProvider(FlowsDataService),
|
||||
mockProvider(IntervalService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
implicitFlowCallbackService = TestBed.inject(ImplicitFlowCallbackService);
|
||||
intervalService = TestBed.inject(IntervalService);
|
||||
flowsDataService = TestBed.inject(FlowsDataService);
|
||||
flowsService = TestBed.inject(FlowsService);
|
||||
router = TestBed.inject(Router);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(implicitFlowCallbackService).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('authorizedImplicitFlowCallback', () => {
|
||||
it('calls flowsService.processImplicitFlowCallback with hash if given', () => {
|
||||
const spy = spyOn(
|
||||
flowsService,
|
||||
'processImplicitFlowCallback'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
triggerAuthorizationResultEvent: true,
|
||||
};
|
||||
|
||||
implicitFlowCallbackService.authenticatedImplicitFlowCallback(
|
||||
config,
|
||||
[config],
|
||||
'some-hash'
|
||||
);
|
||||
|
||||
expect(spy).toHaveBeenCalledOnceWith(config, [config], 'some-hash');
|
||||
});
|
||||
|
||||
it('does nothing if triggerAuthorizationResultEvent is true and isRenewProcess is true', waitForAsync(() => {
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: '',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: null,
|
||||
isRenewProcess: true,
|
||||
jwtKeys: null,
|
||||
validationResult: null,
|
||||
existingIdToken: '',
|
||||
};
|
||||
const spy = spyOn(
|
||||
flowsService,
|
||||
'processImplicitFlowCallback'
|
||||
).and.returnValue(of(callbackContext));
|
||||
const routerSpy = spyOn(router, 'navigateByUrl');
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
triggerAuthorizationResultEvent: true,
|
||||
};
|
||||
|
||||
implicitFlowCallbackService
|
||||
.authenticatedImplicitFlowCallback(config, [config], 'some-hash')
|
||||
.subscribe(() => {
|
||||
expect(spy).toHaveBeenCalledOnceWith(config, [config], 'some-hash');
|
||||
expect(routerSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls router if triggerAuthorizationResultEvent is false and isRenewProcess is false', waitForAsync(() => {
|
||||
const callbackContext = {
|
||||
code: '',
|
||||
refreshToken: '',
|
||||
state: '',
|
||||
sessionState: null,
|
||||
authResult: null,
|
||||
isRenewProcess: false,
|
||||
jwtKeys: null,
|
||||
validationResult: null,
|
||||
existingIdToken: '',
|
||||
};
|
||||
const spy = spyOn(
|
||||
flowsService,
|
||||
'processImplicitFlowCallback'
|
||||
).and.returnValue(of(callbackContext));
|
||||
const routerSpy = spyOn(router, 'navigateByUrl');
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
triggerAuthorizationResultEvent: false,
|
||||
postLoginRoute: 'postLoginRoute',
|
||||
};
|
||||
|
||||
implicitFlowCallbackService
|
||||
.authenticatedImplicitFlowCallback(config, [config], 'some-hash')
|
||||
.subscribe(() => {
|
||||
expect(spy).toHaveBeenCalledOnceWith(config, [config], 'some-hash');
|
||||
expect(routerSpy).toHaveBeenCalledOnceWith('postLoginRoute');
|
||||
});
|
||||
}));
|
||||
|
||||
it('resetSilentRenewRunning and stopPeriodicallyTokenCheck in case of error', waitForAsync(() => {
|
||||
spyOn(flowsService, 'processImplicitFlowCallback').and.returnValue(
|
||||
throwError(() => new Error('error'))
|
||||
);
|
||||
const resetSilentRenewRunningSpy = spyOn(
|
||||
flowsDataService,
|
||||
'resetSilentRenewRunning'
|
||||
);
|
||||
const stopPeriodicallyTokenCheckSpy = spyOn(
|
||||
intervalService,
|
||||
'stopPeriodicTokenCheck'
|
||||
);
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
triggerAuthorizationResultEvent: false,
|
||||
postLoginRoute: 'postLoginRoute',
|
||||
};
|
||||
|
||||
implicitFlowCallbackService
|
||||
.authenticatedImplicitFlowCallback(config, [config], 'some-hash')
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
|
||||
expect(stopPeriodicallyTokenCheckSpy).toHaveBeenCalled();
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it(`navigates to unauthorizedRoute in case of error and in case of error and
|
||||
triggerAuthorizationResultEvent is false`, waitForAsync(() => {
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
|
||||
spyOn(flowsService, 'processImplicitFlowCallback').and.returnValue(
|
||||
throwError(() => new Error('error'))
|
||||
);
|
||||
const resetSilentRenewRunningSpy = spyOn(
|
||||
flowsDataService,
|
||||
'resetSilentRenewRunning'
|
||||
);
|
||||
const stopPeriodicallTokenCheckSpy = spyOn(
|
||||
intervalService,
|
||||
'stopPeriodicTokenCheck'
|
||||
);
|
||||
const routerSpy = spyOn(router, 'navigateByUrl');
|
||||
const config = {
|
||||
configId: 'configId1',
|
||||
triggerAuthorizationResultEvent: false,
|
||||
unauthorizedRoute: 'unauthorizedRoute',
|
||||
};
|
||||
|
||||
implicitFlowCallbackService
|
||||
.authenticatedImplicitFlowCallback(config, [config], 'some-hash')
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
|
||||
expect(stopPeriodicallTokenCheckSpy).toHaveBeenCalled();
|
||||
expect(err).toBeTruthy();
|
||||
expect(routerSpy).toHaveBeenCalledOnceWith('unauthorizedRoute');
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
55
src/callback/implicit-flow-callback.service.ts
Normal file
55
src/callback/implicit-flow-callback.service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
import { OpenIdConfiguration } from '../config/openid-configuration';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowsDataService } from '../flows/flows-data.service';
|
||||
import { FlowsService } from '../flows/flows.service';
|
||||
import { IntervalService } from './interval.service';
|
||||
|
||||
@Injectable()
|
||||
export class ImplicitFlowCallbackService {
|
||||
private readonly flowsService = inject(FlowsService);
|
||||
|
||||
private readonly router = inject(Router);
|
||||
|
||||
private readonly flowsDataService = inject(FlowsDataService);
|
||||
|
||||
private readonly intervalService = inject(IntervalService);
|
||||
|
||||
authenticatedImplicitFlowCallback(
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[],
|
||||
hash?: string
|
||||
): Observable<CallbackContext> {
|
||||
const isRenewProcess = this.flowsDataService.isSilentRenewRunning(config);
|
||||
const triggerAuthorizationResultEvent = Boolean(
|
||||
config.triggerAuthorizationResultEvent
|
||||
);
|
||||
const postLoginRoute = config.postLoginRoute ?? '';
|
||||
const unauthorizedRoute = config.unauthorizedRoute ?? '';
|
||||
|
||||
return this.flowsService
|
||||
.processImplicitFlowCallback(config, allConfigs, hash)
|
||||
.pipe(
|
||||
tap((callbackContext) => {
|
||||
if (
|
||||
!triggerAuthorizationResultEvent &&
|
||||
!callbackContext.isRenewProcess
|
||||
) {
|
||||
this.router.navigateByUrl(postLoginRoute);
|
||||
}
|
||||
}),
|
||||
catchError((error) => {
|
||||
this.flowsDataService.resetSilentRenewRunning(config);
|
||||
this.intervalService.stopPeriodicTokenCheck();
|
||||
if (!triggerAuthorizationResultEvent && !isRenewProcess) {
|
||||
this.router.navigateByUrl(unauthorizedRoute);
|
||||
}
|
||||
|
||||
return throwError(() => new Error(error));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
76
src/callback/interval.service.spec.ts
Normal file
76
src/callback/interval.service.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { IntervalService } from './interval.service';
|
||||
|
||||
describe('IntervalService', () => {
|
||||
let intervalService: IntervalService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: Document,
|
||||
useValue: {
|
||||
defaultView: {
|
||||
setInterval: window.setInterval,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
intervalService = TestBed.inject(IntervalService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(intervalService).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('stopPeriodicTokenCheck', () => {
|
||||
it('calls unsubscribe and sets to null', () => {
|
||||
intervalService.runTokenValidationRunning = new Subscription();
|
||||
const spy = spyOn(
|
||||
intervalService.runTokenValidationRunning,
|
||||
'unsubscribe'
|
||||
);
|
||||
|
||||
intervalService.stopPeriodicTokenCheck();
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(intervalService.runTokenValidationRunning).toBeNull();
|
||||
});
|
||||
|
||||
it('does nothing if `runTokenValidationRunning` is null', () => {
|
||||
intervalService.runTokenValidationRunning = new Subscription();
|
||||
const spy = spyOn(
|
||||
intervalService.runTokenValidationRunning,
|
||||
'unsubscribe'
|
||||
);
|
||||
|
||||
intervalService.runTokenValidationRunning = null;
|
||||
intervalService.stopPeriodicTokenCheck();
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('startPeriodicTokenCheck', () => {
|
||||
it('starts check after correct milliseconds', fakeAsync(() => {
|
||||
const periodicCheck = intervalService.startPeriodicTokenCheck(0.5);
|
||||
const spy = jasmine.createSpy();
|
||||
const sub = periodicCheck.subscribe(() => {
|
||||
spy();
|
||||
});
|
||||
|
||||
tick(500);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
|
||||
tick(500);
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
|
||||
sub.unsubscribe();
|
||||
}));
|
||||
});
|
||||
});
|
||||
42
src/callback/interval.service.ts
Normal file
42
src/callback/interval.service.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Injectable, NgZone, inject } from 'injection-js';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { DOCUMENT } from '../../dom';
|
||||
|
||||
@Injectable()
|
||||
export class IntervalService {
|
||||
private readonly zone = inject(NgZone);
|
||||
|
||||
private readonly document = inject(DOCUMENT);
|
||||
|
||||
runTokenValidationRunning: Subscription | null = null;
|
||||
|
||||
isTokenValidationRunning(): boolean {
|
||||
return Boolean(this.runTokenValidationRunning);
|
||||
}
|
||||
|
||||
stopPeriodicTokenCheck(): void {
|
||||
if (this.runTokenValidationRunning) {
|
||||
this.runTokenValidationRunning.unsubscribe();
|
||||
this.runTokenValidationRunning = null;
|
||||
}
|
||||
}
|
||||
|
||||
startPeriodicTokenCheck(repeatAfterSeconds: number): Observable<unknown> {
|
||||
const millisecondsDelayBetweenTokenCheck = repeatAfterSeconds * 1000;
|
||||
|
||||
return new Observable((subscriber) => {
|
||||
let intervalId: number | undefined;
|
||||
|
||||
this.zone.runOutsideAngular(() => {
|
||||
intervalId = this.document?.defaultView?.setInterval(
|
||||
() => this.zone.run(() => subscriber.next()),
|
||||
millisecondsDelayBetweenTokenCheck
|
||||
);
|
||||
});
|
||||
|
||||
return (): void => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
432
src/callback/periodically-token-check.service.spec.ts
Normal file
432
src/callback/periodically-token-check.service.spec.ts
Normal file
@@ -0,0 +1,432 @@
|
||||
import { fakeAsync, TestBed, tick, 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 { ConfigurationService } from '../config/config.service';
|
||||
import { OpenIdConfiguration } from '../config/openid-configuration';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowsDataService } from '../flows/flows-data.service';
|
||||
import { ResetAuthDataService } from '../flows/reset-auth-data.service';
|
||||
import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { EventTypes } from '../public-events/event-types';
|
||||
import { PublicEventsService } from '../public-events/public-events.service';
|
||||
import { StoragePersistenceService } from '../storage/storage-persistence.service';
|
||||
import { UserService } from '../user-data/user.service';
|
||||
import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
|
||||
import { IntervalService } from './interval.service';
|
||||
import { PeriodicallyTokenCheckService } from './periodically-token-check.service';
|
||||
import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service';
|
||||
|
||||
describe('PeriodicallyTokenCheckService', () => {
|
||||
let periodicallyTokenCheckService: PeriodicallyTokenCheckService;
|
||||
let intervalService: IntervalService;
|
||||
let flowsDataService: FlowsDataService;
|
||||
let flowHelper: FlowHelper;
|
||||
let authStateService: AuthStateService;
|
||||
let refreshSessionRefreshTokenService: RefreshSessionRefreshTokenService;
|
||||
let userService: UserService;
|
||||
let storagePersistenceService: StoragePersistenceService;
|
||||
let resetAuthDataService: ResetAuthDataService;
|
||||
let configurationService: ConfigurationService;
|
||||
let publicEventsService: PublicEventsService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
mockProvider(ResetAuthDataService),
|
||||
FlowHelper,
|
||||
mockProvider(FlowsDataService),
|
||||
mockProvider(LoggerService),
|
||||
mockProvider(UserService),
|
||||
mockProvider(AuthStateService),
|
||||
mockProvider(RefreshSessionIframeService),
|
||||
mockProvider(RefreshSessionRefreshTokenService),
|
||||
mockProvider(IntervalService),
|
||||
mockProvider(StoragePersistenceService),
|
||||
mockProvider(PublicEventsService),
|
||||
mockProvider(ConfigurationService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
periodicallyTokenCheckService = TestBed.inject(
|
||||
PeriodicallyTokenCheckService
|
||||
);
|
||||
intervalService = TestBed.inject(IntervalService);
|
||||
flowsDataService = TestBed.inject(FlowsDataService);
|
||||
flowHelper = TestBed.inject(FlowHelper);
|
||||
authStateService = TestBed.inject(AuthStateService);
|
||||
refreshSessionRefreshTokenService = TestBed.inject(
|
||||
RefreshSessionRefreshTokenService
|
||||
);
|
||||
userService = TestBed.inject(UserService);
|
||||
storagePersistenceService = TestBed.inject(StoragePersistenceService);
|
||||
resetAuthDataService = TestBed.inject(ResetAuthDataService);
|
||||
publicEventsService = TestBed.inject(PublicEventsService);
|
||||
configurationService = TestBed.inject(ConfigurationService);
|
||||
|
||||
spyOn(intervalService, 'startPeriodicTokenCheck').and.returnValue(of(null));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (!!intervalService.runTokenValidationRunning?.unsubscribe) {
|
||||
intervalService.runTokenValidationRunning.unsubscribe();
|
||||
intervalService.runTokenValidationRunning = null;
|
||||
}
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(periodicallyTokenCheckService).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('startTokenValidationPeriodically', () => {
|
||||
it('returns if no config has silentrenew enabled', waitForAsync(() => {
|
||||
const configs = [
|
||||
{ silentRenew: false, configId: 'configId1' },
|
||||
{ silentRenew: false, configId: 'configId2' },
|
||||
];
|
||||
|
||||
const result =
|
||||
periodicallyTokenCheckService.startTokenValidationPeriodically(
|
||||
configs,
|
||||
configs[0]
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
}));
|
||||
|
||||
it('returns if runTokenValidationRunning', waitForAsync(() => {
|
||||
const configs = [{ silentRenew: true, configId: 'configId1' }];
|
||||
|
||||
spyOn(intervalService, 'isTokenValidationRunning').and.returnValue(true);
|
||||
|
||||
const result =
|
||||
periodicallyTokenCheckService.startTokenValidationPeriodically(
|
||||
configs,
|
||||
configs[0]
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
}));
|
||||
|
||||
it('interval calls resetSilentRenewRunning when current flow is CodeFlowWithRefreshTokens', fakeAsync(() => {
|
||||
const configs = [
|
||||
{ silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 },
|
||||
];
|
||||
|
||||
spyOn(
|
||||
periodicallyTokenCheckService as any,
|
||||
'shouldStartPeriodicallyCheckForConfig'
|
||||
).and.returnValue(true);
|
||||
const isCurrentFlowCodeFlowWithRefreshTokensSpy = spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(true);
|
||||
const resetSilentRenewRunningSpy = spyOn(
|
||||
flowsDataService,
|
||||
'resetSilentRenewRunning'
|
||||
);
|
||||
|
||||
spyOn(
|
||||
refreshSessionRefreshTokenService,
|
||||
'refreshSessionWithRefreshTokens'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue(
|
||||
of(configs[0])
|
||||
);
|
||||
|
||||
periodicallyTokenCheckService.startTokenValidationPeriodically(
|
||||
configs,
|
||||
configs[0]
|
||||
);
|
||||
|
||||
tick(1000);
|
||||
|
||||
intervalService.runTokenValidationRunning?.unsubscribe();
|
||||
intervalService.runTokenValidationRunning = null;
|
||||
expect(isCurrentFlowCodeFlowWithRefreshTokensSpy).toHaveBeenCalled();
|
||||
expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('interval calls resetSilentRenewRunning in case of error when current flow is CodeFlowWithRefreshTokens', fakeAsync(() => {
|
||||
const configs = [
|
||||
{ silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 },
|
||||
];
|
||||
|
||||
spyOn(
|
||||
periodicallyTokenCheckService as any,
|
||||
'shouldStartPeriodicallyCheckForConfig'
|
||||
).and.returnValue(true);
|
||||
const resetSilentRenewRunning = spyOn(
|
||||
flowsDataService,
|
||||
'resetSilentRenewRunning'
|
||||
);
|
||||
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(true);
|
||||
spyOn(
|
||||
refreshSessionRefreshTokenService,
|
||||
'refreshSessionWithRefreshTokens'
|
||||
).and.returnValue(throwError(() => new Error('error')));
|
||||
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue(
|
||||
of(configs[0])
|
||||
);
|
||||
|
||||
periodicallyTokenCheckService.startTokenValidationPeriodically(
|
||||
configs,
|
||||
configs[0]
|
||||
);
|
||||
|
||||
tick(1000);
|
||||
|
||||
expect(
|
||||
periodicallyTokenCheckService.startTokenValidationPeriodically
|
||||
).toThrowError();
|
||||
expect(resetSilentRenewRunning).toHaveBeenCalledOnceWith(configs[0]);
|
||||
}));
|
||||
|
||||
it('interval throws silent renew failed event with data in case of an error', fakeAsync(() => {
|
||||
const configs = [
|
||||
{ silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 },
|
||||
];
|
||||
|
||||
spyOn(
|
||||
periodicallyTokenCheckService as any,
|
||||
'shouldStartPeriodicallyCheckForConfig'
|
||||
).and.returnValue(true);
|
||||
spyOn(flowsDataService, 'resetSilentRenewRunning');
|
||||
const publicEventsServiceSpy = spyOn(publicEventsService, 'fireEvent');
|
||||
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(true);
|
||||
spyOn(
|
||||
refreshSessionRefreshTokenService,
|
||||
'refreshSessionWithRefreshTokens'
|
||||
).and.returnValue(throwError(() => new Error('error')));
|
||||
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue(
|
||||
of(configs[0])
|
||||
);
|
||||
|
||||
periodicallyTokenCheckService.startTokenValidationPeriodically(
|
||||
configs,
|
||||
configs[0]
|
||||
);
|
||||
|
||||
tick(1000);
|
||||
|
||||
expect(
|
||||
periodicallyTokenCheckService.startTokenValidationPeriodically
|
||||
).toThrowError();
|
||||
expect(publicEventsServiceSpy.calls.allArgs()).toEqual([
|
||||
[EventTypes.SilentRenewStarted],
|
||||
[EventTypes.SilentRenewFailed, new Error('error')],
|
||||
]);
|
||||
}));
|
||||
|
||||
it('calls resetAuthorizationData and returns if no silent renew is configured', fakeAsync(() => {
|
||||
const configs = [
|
||||
{ silentRenew: true, configId: 'configId1', tokenRefreshInSeconds: 1 },
|
||||
];
|
||||
|
||||
spyOn(
|
||||
periodicallyTokenCheckService as any,
|
||||
'shouldStartPeriodicallyCheckForConfig'
|
||||
).and.returnValue(true);
|
||||
|
||||
const configSpy = spyOn(configurationService, 'getOpenIDConfiguration');
|
||||
const configWithoutSilentRenew = {
|
||||
silentRenew: false,
|
||||
configId: 'configId1',
|
||||
tokenRefreshInSeconds: 1,
|
||||
};
|
||||
const configWithoutSilentRenew$ = of(configWithoutSilentRenew);
|
||||
|
||||
configSpy.and.returnValue(configWithoutSilentRenew$);
|
||||
|
||||
const resetAuthorizationDataSpy = spyOn(
|
||||
resetAuthDataService,
|
||||
'resetAuthorizationData'
|
||||
);
|
||||
|
||||
periodicallyTokenCheckService.startTokenValidationPeriodically(
|
||||
configs,
|
||||
configs[0]
|
||||
);
|
||||
tick(1000);
|
||||
intervalService.runTokenValidationRunning?.unsubscribe();
|
||||
intervalService.runTokenValidationRunning = null;
|
||||
|
||||
expect(resetAuthorizationDataSpy).toHaveBeenCalledTimes(1);
|
||||
expect(resetAuthorizationDataSpy).toHaveBeenCalledOnceWith(
|
||||
configWithoutSilentRenew,
|
||||
configs
|
||||
);
|
||||
}));
|
||||
|
||||
it('calls refreshSessionWithRefreshTokens if current flow is Code flow with refresh tokens', fakeAsync(() => {
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(true);
|
||||
spyOn(
|
||||
periodicallyTokenCheckService as any,
|
||||
'shouldStartPeriodicallyCheckForConfig'
|
||||
).and.returnValue(true);
|
||||
spyOn(storagePersistenceService, 'read').and.returnValue({});
|
||||
const configs = [
|
||||
{ configId: 'configId1', silentRenew: true, tokenRefreshInSeconds: 1 },
|
||||
];
|
||||
|
||||
spyOn(configurationService, 'getOpenIDConfiguration').and.returnValue(
|
||||
of(configs[0] as OpenIdConfiguration)
|
||||
);
|
||||
const refreshSessionWithRefreshTokensSpy = spyOn(
|
||||
refreshSessionRefreshTokenService,
|
||||
'refreshSessionWithRefreshTokens'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
|
||||
periodicallyTokenCheckService.startTokenValidationPeriodically(
|
||||
configs,
|
||||
configs[0]
|
||||
);
|
||||
|
||||
tick(1000);
|
||||
|
||||
intervalService.runTokenValidationRunning?.unsubscribe();
|
||||
intervalService.runTokenValidationRunning = null;
|
||||
expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('shouldStartPeriodicallyCheckForConfig', () => {
|
||||
it('returns false when there is no IdToken', () => {
|
||||
spyOn(authStateService, 'getIdToken').and.returnValue('');
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
|
||||
spyOn(userService, 'getUserDataFromStore').and.returnValue(
|
||||
'some-userdata'
|
||||
);
|
||||
|
||||
const result = (
|
||||
periodicallyTokenCheckService as any
|
||||
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
|
||||
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns false when silent renew is running', () => {
|
||||
spyOn(authStateService, 'getIdToken').and.returnValue('idToken');
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
|
||||
spyOn(userService, 'getUserDataFromStore').and.returnValue(
|
||||
'some-userdata'
|
||||
);
|
||||
|
||||
const result = (
|
||||
periodicallyTokenCheckService as any
|
||||
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
|
||||
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns false when code flow is in progress', () => {
|
||||
spyOn(authStateService, 'getIdToken').and.returnValue('idToken');
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
|
||||
spyOn(flowsDataService, 'isCodeFlowInProgress').and.returnValue(true);
|
||||
spyOn(userService, 'getUserDataFromStore').and.returnValue(
|
||||
'some-userdata'
|
||||
);
|
||||
|
||||
const result = (
|
||||
periodicallyTokenCheckService as any
|
||||
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
|
||||
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns false when there is no userdata from the store', () => {
|
||||
spyOn(authStateService, 'getIdToken').and.returnValue('idToken');
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
|
||||
spyOn(userService, 'getUserDataFromStore').and.returnValue(null);
|
||||
|
||||
const result = (
|
||||
periodicallyTokenCheckService as any
|
||||
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
|
||||
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns true when there is userDataFromStore, silentrenew is not running and there is an idtoken', () => {
|
||||
spyOn(authStateService, 'getIdToken').and.returnValue('idToken');
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
|
||||
spyOn(userService, 'getUserDataFromStore').and.returnValue(
|
||||
'some-userdata'
|
||||
);
|
||||
|
||||
spyOn(
|
||||
authStateService,
|
||||
'hasIdTokenExpiredAndRenewCheckIsEnabled'
|
||||
).and.returnValue(true);
|
||||
spyOn(
|
||||
authStateService,
|
||||
'hasAccessTokenExpiredIfExpiryExists'
|
||||
).and.returnValue(true);
|
||||
|
||||
const result = (
|
||||
periodicallyTokenCheckService as any
|
||||
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
|
||||
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
|
||||
it('returns false if tokens are not expired', () => {
|
||||
spyOn(authStateService, 'getIdToken').and.returnValue('idToken');
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
|
||||
spyOn(userService, 'getUserDataFromStore').and.returnValue(
|
||||
'some-userdata'
|
||||
);
|
||||
spyOn(
|
||||
authStateService,
|
||||
'hasIdTokenExpiredAndRenewCheckIsEnabled'
|
||||
).and.returnValue(false);
|
||||
spyOn(
|
||||
authStateService,
|
||||
'hasAccessTokenExpiredIfExpiryExists'
|
||||
).and.returnValue(false);
|
||||
|
||||
const result = (
|
||||
periodicallyTokenCheckService as any
|
||||
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
|
||||
|
||||
expect(result).toBeFalse();
|
||||
});
|
||||
|
||||
it('returns true if tokens are expired', () => {
|
||||
spyOn(authStateService, 'getIdToken').and.returnValue('idToken');
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
|
||||
spyOn(userService, 'getUserDataFromStore').and.returnValue(
|
||||
'some-userdata'
|
||||
);
|
||||
|
||||
spyOn(
|
||||
authStateService,
|
||||
'hasIdTokenExpiredAndRenewCheckIsEnabled'
|
||||
).and.returnValue(true);
|
||||
spyOn(
|
||||
authStateService,
|
||||
'hasAccessTokenExpiredIfExpiryExists'
|
||||
).and.returnValue(true);
|
||||
|
||||
const result = (
|
||||
periodicallyTokenCheckService as any
|
||||
).shouldStartPeriodicallyCheckForConfig({ configId: 'configId1' });
|
||||
|
||||
expect(result).toBeTrue();
|
||||
});
|
||||
});
|
||||
});
|
||||
258
src/callback/periodically-token-check.service.ts
Normal file
258
src/callback/periodically-token-check.service.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { forkJoin, Observable, of, throwError } from 'rxjs';
|
||||
import { catchError, switchMap } from 'rxjs/operators';
|
||||
import { AuthStateService } from '../auth-state/auth-state.service';
|
||||
import { ConfigurationService } from '../config/config.service';
|
||||
import { OpenIdConfiguration } from '../config/openid-configuration';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowsDataService } from '../flows/flows-data.service';
|
||||
import { ResetAuthDataService } from '../flows/reset-auth-data.service';
|
||||
import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { EventTypes } from '../public-events/event-types';
|
||||
import { PublicEventsService } from '../public-events/public-events.service';
|
||||
import { StoragePersistenceService } from '../storage/storage-persistence.service';
|
||||
import { UserService } from '../user-data/user.service';
|
||||
import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
|
||||
import { IntervalService } from './interval.service';
|
||||
import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service';
|
||||
|
||||
@Injectable()
|
||||
export class PeriodicallyTokenCheckService {
|
||||
private readonly resetAuthDataService = inject(ResetAuthDataService);
|
||||
|
||||
private readonly flowHelper = inject(FlowHelper);
|
||||
|
||||
private readonly flowsDataService = inject(FlowsDataService);
|
||||
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly userService = inject(UserService);
|
||||
|
||||
private readonly authStateService = inject(AuthStateService);
|
||||
|
||||
private readonly refreshSessionIframeService = inject(
|
||||
RefreshSessionIframeService
|
||||
);
|
||||
|
||||
private readonly refreshSessionRefreshTokenService = inject(
|
||||
RefreshSessionRefreshTokenService
|
||||
);
|
||||
|
||||
private readonly intervalService = inject(IntervalService);
|
||||
|
||||
private readonly storagePersistenceService = inject(
|
||||
StoragePersistenceService
|
||||
);
|
||||
|
||||
private readonly publicEventsService = inject(PublicEventsService);
|
||||
|
||||
private readonly configurationService = inject(ConfigurationService);
|
||||
|
||||
startTokenValidationPeriodically(
|
||||
allConfigs: OpenIdConfiguration[],
|
||||
currentConfig: OpenIdConfiguration
|
||||
): void {
|
||||
const configsWithSilentRenewEnabled =
|
||||
this.getConfigsWithSilentRenewEnabled(allConfigs);
|
||||
|
||||
if (configsWithSilentRenewEnabled.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.intervalService.isTokenValidationRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshTimeInSeconds = this.getSmallestRefreshTimeFromConfigs(
|
||||
configsWithSilentRenewEnabled
|
||||
);
|
||||
const periodicallyCheck$ = this.intervalService
|
||||
.startPeriodicTokenCheck(refreshTimeInSeconds)
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
const objectWithConfigIdsAndRefreshEvent: {
|
||||
[id: string]: Observable<boolean | CallbackContext | null>;
|
||||
} = {};
|
||||
|
||||
configsWithSilentRenewEnabled.forEach((config) => {
|
||||
const identifier = config.configId as string;
|
||||
const refreshEvent = this.getRefreshEvent(config, allConfigs);
|
||||
|
||||
objectWithConfigIdsAndRefreshEvent[identifier] = refreshEvent;
|
||||
});
|
||||
|
||||
return forkJoin(objectWithConfigIdsAndRefreshEvent);
|
||||
})
|
||||
);
|
||||
|
||||
this.intervalService.runTokenValidationRunning = periodicallyCheck$
|
||||
.pipe(catchError((error) => throwError(() => new Error(error))))
|
||||
.subscribe({
|
||||
next: (objectWithConfigIds) => {
|
||||
for (const [configId, _] of Object.entries(objectWithConfigIds)) {
|
||||
this.configurationService
|
||||
.getOpenIDConfiguration(configId)
|
||||
.subscribe((config) => {
|
||||
this.loggerService.logDebug(
|
||||
config,
|
||||
'silent renew, periodic check finished!'
|
||||
);
|
||||
|
||||
if (
|
||||
this.flowHelper.isCurrentFlowCodeFlowWithRefreshTokens(config)
|
||||
) {
|
||||
this.flowsDataService.resetSilentRenewRunning(config);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.loggerService.logError(
|
||||
currentConfig,
|
||||
'silent renew failed!',
|
||||
error
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private getRefreshEvent(
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[]
|
||||
): Observable<boolean | CallbackContext | null> {
|
||||
const shouldStartRefreshEvent =
|
||||
this.shouldStartPeriodicallyCheckForConfig(config);
|
||||
|
||||
if (!shouldStartRefreshEvent) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
const refreshEvent$ = this.createRefreshEventForConfig(config, allConfigs);
|
||||
|
||||
this.publicEventsService.fireEvent(EventTypes.SilentRenewStarted);
|
||||
|
||||
return refreshEvent$.pipe(
|
||||
catchError((error) => {
|
||||
this.loggerService.logError(config, 'silent renew failed!', error);
|
||||
this.publicEventsService.fireEvent(EventTypes.SilentRenewFailed, error);
|
||||
this.flowsDataService.resetSilentRenewRunning(config);
|
||||
|
||||
return throwError(() => new Error(error));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getSmallestRefreshTimeFromConfigs(
|
||||
configsWithSilentRenewEnabled: OpenIdConfiguration[]
|
||||
): number {
|
||||
const result = configsWithSilentRenewEnabled.reduce((prev, curr) =>
|
||||
(prev.tokenRefreshInSeconds ?? 0) < (curr.tokenRefreshInSeconds ?? 0)
|
||||
? prev
|
||||
: curr
|
||||
);
|
||||
|
||||
return result.tokenRefreshInSeconds ?? 0;
|
||||
}
|
||||
|
||||
private getConfigsWithSilentRenewEnabled(
|
||||
allConfigs: OpenIdConfiguration[]
|
||||
): OpenIdConfiguration[] {
|
||||
return allConfigs.filter((x) => x.silentRenew);
|
||||
}
|
||||
|
||||
private createRefreshEventForConfig(
|
||||
configuration: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[]
|
||||
): Observable<boolean | CallbackContext | null> {
|
||||
this.loggerService.logDebug(configuration, 'starting silent renew...');
|
||||
|
||||
return this.configurationService
|
||||
.getOpenIDConfiguration(configuration.configId)
|
||||
.pipe(
|
||||
switchMap((config) => {
|
||||
if (!config?.silentRenew) {
|
||||
this.resetAuthDataService.resetAuthorizationData(
|
||||
config,
|
||||
allConfigs
|
||||
);
|
||||
|
||||
return of(null);
|
||||
}
|
||||
|
||||
this.flowsDataService.setSilentRenewRunning(config);
|
||||
|
||||
if (this.flowHelper.isCurrentFlowCodeFlowWithRefreshTokens(config)) {
|
||||
// Retrieve Dynamically Set Custom Params for refresh body
|
||||
const customParamsRefresh: {
|
||||
[key: string]: string | number | boolean;
|
||||
} =
|
||||
this.storagePersistenceService.read(
|
||||
'storageCustomParamsRefresh',
|
||||
config
|
||||
) || {};
|
||||
|
||||
const { customParamsRefreshTokenRequest } = config;
|
||||
|
||||
const mergedParams = {
|
||||
...customParamsRefreshTokenRequest,
|
||||
...customParamsRefresh,
|
||||
};
|
||||
|
||||
// Refresh Session using Refresh tokens
|
||||
return this.refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens(
|
||||
config,
|
||||
allConfigs,
|
||||
mergedParams
|
||||
);
|
||||
}
|
||||
|
||||
// Retrieve Dynamically Set Custom Params
|
||||
const customParams: { [key: string]: string | number | boolean } =
|
||||
this.storagePersistenceService.read(
|
||||
'storageCustomParamsAuthRequest',
|
||||
config
|
||||
);
|
||||
|
||||
return this.refreshSessionIframeService.refreshSessionWithIframe(
|
||||
config,
|
||||
allConfigs,
|
||||
customParams
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private shouldStartPeriodicallyCheckForConfig(
|
||||
config: OpenIdConfiguration
|
||||
): boolean {
|
||||
const idToken = this.authStateService.getIdToken(config);
|
||||
const isSilentRenewRunning =
|
||||
this.flowsDataService.isSilentRenewRunning(config);
|
||||
const isCodeFlowInProgress =
|
||||
this.flowsDataService.isCodeFlowInProgress(config);
|
||||
const userDataFromStore = this.userService.getUserDataFromStore(config);
|
||||
|
||||
this.loggerService.logDebug(
|
||||
config,
|
||||
`Checking: silentRenewRunning: ${isSilentRenewRunning}, isCodeFlowInProgress: ${isCodeFlowInProgress} - has idToken: ${!!idToken} - has userData: ${!!userDataFromStore}`
|
||||
);
|
||||
|
||||
const shouldBeExecuted =
|
||||
!!userDataFromStore &&
|
||||
!isSilentRenewRunning &&
|
||||
!!idToken &&
|
||||
!isCodeFlowInProgress;
|
||||
|
||||
if (!shouldBeExecuted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const idTokenExpired =
|
||||
this.authStateService.hasIdTokenExpiredAndRenewCheckIsEnabled(config);
|
||||
const accessTokenExpired =
|
||||
this.authStateService.hasAccessTokenExpiredIfExpiryExists(config);
|
||||
|
||||
return idTokenExpired || accessTokenExpired;
|
||||
}
|
||||
}
|
||||
101
src/callback/refresh-session-refresh-token.service.spec.ts
Normal file
101
src/callback/refresh-session-refresh-token.service.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { mockProvider } from '../../test/auto-mock';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowsService } from '../flows/flows.service';
|
||||
import { ResetAuthDataService } from '../flows/reset-auth-data.service';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { IntervalService } from './interval.service';
|
||||
import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service';
|
||||
|
||||
describe('RefreshSessionRefreshTokenService', () => {
|
||||
let refreshSessionRefreshTokenService: RefreshSessionRefreshTokenService;
|
||||
let intervalService: IntervalService;
|
||||
let resetAuthDataService: ResetAuthDataService;
|
||||
let flowsService: FlowsService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
RefreshSessionRefreshTokenService,
|
||||
mockProvider(LoggerService),
|
||||
mockProvider(FlowsService),
|
||||
mockProvider(ResetAuthDataService),
|
||||
mockProvider(IntervalService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
flowsService = TestBed.inject(FlowsService);
|
||||
refreshSessionRefreshTokenService = TestBed.inject(
|
||||
RefreshSessionRefreshTokenService
|
||||
);
|
||||
intervalService = TestBed.inject(IntervalService);
|
||||
resetAuthDataService = TestBed.inject(ResetAuthDataService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(refreshSessionRefreshTokenService).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('refreshSessionWithRefreshTokens', () => {
|
||||
it('calls flowsService.processRefreshToken()', waitForAsync(() => {
|
||||
const spy = spyOn(flowsService, 'processRefreshToken').and.returnValue(
|
||||
of({} as CallbackContext)
|
||||
);
|
||||
|
||||
refreshSessionRefreshTokenService
|
||||
.refreshSessionWithRefreshTokens({ configId: 'configId1' }, [
|
||||
{ configId: 'configId1' },
|
||||
])
|
||||
.subscribe(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('resetAuthorizationData in case of error', waitForAsync(() => {
|
||||
spyOn(flowsService, 'processRefreshToken').and.returnValue(
|
||||
throwError(() => new Error('error'))
|
||||
);
|
||||
const resetSilentRenewRunningSpy = spyOn(
|
||||
resetAuthDataService,
|
||||
'resetAuthorizationData'
|
||||
);
|
||||
|
||||
refreshSessionRefreshTokenService
|
||||
.refreshSessionWithRefreshTokens({ configId: 'configId1' }, [
|
||||
{ configId: 'configId1' },
|
||||
])
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(resetSilentRenewRunningSpy).toHaveBeenCalled();
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('finalize with stopPeriodicTokenCheck in case of error', fakeAsync(() => {
|
||||
spyOn(flowsService, 'processRefreshToken').and.returnValue(
|
||||
throwError(() => new Error('error'))
|
||||
);
|
||||
const stopPeriodicallyTokenCheckSpy = spyOn(
|
||||
intervalService,
|
||||
'stopPeriodicTokenCheck'
|
||||
);
|
||||
|
||||
refreshSessionRefreshTokenService
|
||||
.refreshSessionWithRefreshTokens({ configId: 'configId1' }, [
|
||||
{ configId: 'configId1' },
|
||||
])
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
expect(err).toBeTruthy();
|
||||
},
|
||||
});
|
||||
tick();
|
||||
expect(stopPeriodicallyTokenCheckSpy).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
});
|
||||
44
src/callback/refresh-session-refresh-token.service.ts
Normal file
44
src/callback/refresh-session-refresh-token.service.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, finalize } from 'rxjs/operators';
|
||||
import { OpenIdConfiguration } from '../config/openid-configuration';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowsService } from '../flows/flows.service';
|
||||
import { ResetAuthDataService } from '../flows/reset-auth-data.service';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { IntervalService } from './interval.service';
|
||||
|
||||
@Injectable()
|
||||
export class RefreshSessionRefreshTokenService {
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly resetAuthDataService = inject(ResetAuthDataService);
|
||||
|
||||
private readonly flowsService = inject(FlowsService);
|
||||
|
||||
private readonly intervalService = inject(IntervalService);
|
||||
|
||||
refreshSessionWithRefreshTokens(
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[],
|
||||
customParamsRefresh?: { [key: string]: string | number | boolean }
|
||||
): Observable<CallbackContext> {
|
||||
this.loggerService.logDebug(config, 'BEGIN refresh session Authorize');
|
||||
let refreshTokenFailed = false;
|
||||
|
||||
return this.flowsService
|
||||
.processRefreshToken(config, allConfigs, customParamsRefresh)
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
this.resetAuthDataService.resetAuthorizationData(config, allConfigs);
|
||||
refreshTokenFailed = true;
|
||||
|
||||
return throwError(() => new Error(error));
|
||||
}),
|
||||
finalize(
|
||||
() =>
|
||||
refreshTokenFailed && this.intervalService.stopPeriodicTokenCheck()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
667
src/callback/refresh-session.service.spec.ts
Normal file
667
src/callback/refresh-session.service.spec.ts
Normal file
@@ -0,0 +1,667 @@
|
||||
import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { delay } from 'rxjs/operators';
|
||||
import { mockProvider } from '../../test/auto-mock';
|
||||
import { AuthStateService } from '../auth-state/auth-state.service';
|
||||
import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowsDataService } from '../flows/flows-data.service';
|
||||
import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service';
|
||||
import { SilentRenewService } from '../iframe/silent-renew.service';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { LoginResponse } from '../login/login-response';
|
||||
import { PublicEventsService } from '../public-events/public-events.service';
|
||||
import { StoragePersistenceService } from '../storage/storage-persistence.service';
|
||||
import { UserService } from '../user-data/user.service';
|
||||
import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
|
||||
import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service';
|
||||
import {
|
||||
MAX_RETRY_ATTEMPTS,
|
||||
RefreshSessionService,
|
||||
} from './refresh-session.service';
|
||||
|
||||
describe('RefreshSessionService ', () => {
|
||||
let refreshSessionService: RefreshSessionService;
|
||||
let flowHelper: FlowHelper;
|
||||
let authStateService: AuthStateService;
|
||||
let silentRenewService: SilentRenewService;
|
||||
let storagePersistenceService: StoragePersistenceService;
|
||||
let flowsDataService: FlowsDataService;
|
||||
let refreshSessionIframeService: RefreshSessionIframeService;
|
||||
let refreshSessionRefreshTokenService: RefreshSessionRefreshTokenService;
|
||||
let authWellKnownService: AuthWellKnownService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
FlowHelper,
|
||||
mockProvider(FlowsDataService),
|
||||
RefreshSessionService,
|
||||
mockProvider(LoggerService),
|
||||
mockProvider(SilentRenewService),
|
||||
mockProvider(AuthStateService),
|
||||
mockProvider(AuthWellKnownService),
|
||||
mockProvider(RefreshSessionIframeService),
|
||||
mockProvider(StoragePersistenceService),
|
||||
mockProvider(RefreshSessionRefreshTokenService),
|
||||
mockProvider(UserService),
|
||||
mockProvider(PublicEventsService),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
refreshSessionService = TestBed.inject(RefreshSessionService);
|
||||
flowsDataService = TestBed.inject(FlowsDataService);
|
||||
flowHelper = TestBed.inject(FlowHelper);
|
||||
authStateService = TestBed.inject(AuthStateService);
|
||||
refreshSessionIframeService = TestBed.inject(RefreshSessionIframeService);
|
||||
refreshSessionRefreshTokenService = TestBed.inject(
|
||||
RefreshSessionRefreshTokenService
|
||||
);
|
||||
silentRenewService = TestBed.inject(SilentRenewService);
|
||||
authWellKnownService = TestBed.inject(AuthWellKnownService);
|
||||
storagePersistenceService = TestBed.inject(StoragePersistenceService);
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(refreshSessionService).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('userForceRefreshSession', () => {
|
||||
it('should persist params refresh when extra custom params given and useRefreshToken is true', waitForAsync(() => {
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(true);
|
||||
spyOn(
|
||||
refreshSessionService as any,
|
||||
'startRefreshSession'
|
||||
).and.returnValue(of(null));
|
||||
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
|
||||
true
|
||||
);
|
||||
const writeSpy = spyOn(storagePersistenceService, 'write');
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
useRefreshToken: true,
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
|
||||
const extraCustomParams = { extra: 'custom' };
|
||||
|
||||
refreshSessionService
|
||||
.userForceRefreshSession(allConfigs[0], allConfigs, extraCustomParams)
|
||||
.subscribe(() => {
|
||||
expect(writeSpy).toHaveBeenCalledOnceWith(
|
||||
'storageCustomParamsRefresh',
|
||||
extraCustomParams,
|
||||
allConfigs[0]
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should persist storageCustomParamsAuthRequest when extra custom params given and useRefreshToken is false', waitForAsync(() => {
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(true);
|
||||
spyOn(
|
||||
refreshSessionService as any,
|
||||
'startRefreshSession'
|
||||
).and.returnValue(of(null));
|
||||
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
|
||||
true
|
||||
);
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
useRefreshToken: false,
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
const writeSpy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
const extraCustomParams = { extra: 'custom' };
|
||||
|
||||
refreshSessionService
|
||||
.userForceRefreshSession(allConfigs[0], allConfigs, extraCustomParams)
|
||||
.subscribe(() => {
|
||||
expect(writeSpy).toHaveBeenCalledOnceWith(
|
||||
'storageCustomParamsAuthRequest',
|
||||
extraCustomParams,
|
||||
allConfigs[0]
|
||||
);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should NOT persist customparams if no customparams are given', waitForAsync(() => {
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(true);
|
||||
spyOn(
|
||||
refreshSessionService as any,
|
||||
'startRefreshSession'
|
||||
).and.returnValue(of(null));
|
||||
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
|
||||
true
|
||||
);
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
useRefreshToken: false,
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
const writeSpy = spyOn(storagePersistenceService, 'write');
|
||||
|
||||
refreshSessionService
|
||||
.userForceRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe(() => {
|
||||
expect(writeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should call resetSilentRenewRunning in case of an error', waitForAsync(() => {
|
||||
spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue(
|
||||
throwError(() => new Error('error'))
|
||||
);
|
||||
spyOn(flowsDataService, 'resetSilentRenewRunning');
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
useRefreshToken: false,
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
|
||||
refreshSessionService
|
||||
.userForceRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
fail('It should not return any result.');
|
||||
},
|
||||
error: (error) => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
},
|
||||
complete: () => {
|
||||
expect(
|
||||
flowsDataService.resetSilentRenewRunning
|
||||
).toHaveBeenCalledOnceWith(allConfigs[0]);
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('should call resetSilentRenewRunning in case of no error', waitForAsync(() => {
|
||||
spyOn(refreshSessionService, 'forceRefreshSession').and.returnValue(
|
||||
of({} as LoginResponse)
|
||||
);
|
||||
spyOn(flowsDataService, 'resetSilentRenewRunning');
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
useRefreshToken: false,
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
|
||||
refreshSessionService
|
||||
.userForceRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe({
|
||||
error: () => {
|
||||
fail('It should not return any error.');
|
||||
},
|
||||
complete: () => {
|
||||
expect(
|
||||
flowsDataService.resetSilentRenewRunning
|
||||
).toHaveBeenCalledOnceWith(allConfigs[0]);
|
||||
},
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('forceRefreshSession', () => {
|
||||
it('only calls start refresh session and returns idToken and accessToken if auth is true', waitForAsync(() => {
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(true);
|
||||
spyOn(
|
||||
refreshSessionService as any,
|
||||
'startRefreshSession'
|
||||
).and.returnValue(of(null));
|
||||
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
|
||||
true
|
||||
);
|
||||
spyOn(authStateService, 'getIdToken').and.returnValue('id-token');
|
||||
spyOn(authStateService, 'getAccessToken').and.returnValue('access-token');
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
|
||||
refreshSessionService
|
||||
.forceRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe((result) => {
|
||||
expect(result.idToken).toEqual('id-token');
|
||||
expect(result.accessToken).toEqual('access-token');
|
||||
});
|
||||
}));
|
||||
|
||||
it('only calls start refresh session and returns null if auth is false', waitForAsync(() => {
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(true);
|
||||
spyOn(
|
||||
refreshSessionService as any,
|
||||
'startRefreshSession'
|
||||
).and.returnValue(of(null));
|
||||
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
|
||||
false
|
||||
);
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
|
||||
refreshSessionService
|
||||
.forceRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe((result) => {
|
||||
expect(result).toEqual({
|
||||
isAuthenticated: false,
|
||||
errorMessage: '',
|
||||
userData: null,
|
||||
idToken: '',
|
||||
accessToken: '',
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls start refresh session and waits for completed, returns idtoken and accesstoken if auth is true', waitForAsync(() => {
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(false);
|
||||
spyOn(
|
||||
refreshSessionService as any,
|
||||
'startRefreshSession'
|
||||
).and.returnValue(of(null));
|
||||
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
|
||||
true
|
||||
);
|
||||
spyOnProperty(
|
||||
silentRenewService,
|
||||
'refreshSessionWithIFrameCompleted$'
|
||||
).and.returnValue(
|
||||
of({
|
||||
authResult: {
|
||||
id_token: 'some-id_token',
|
||||
access_token: 'some-access_token',
|
||||
},
|
||||
} as CallbackContext)
|
||||
);
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
|
||||
refreshSessionService
|
||||
.forceRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe((result) => {
|
||||
expect(result.idToken).toBeDefined();
|
||||
expect(result.accessToken).toBeDefined();
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls start refresh session and waits for completed, returns LoginResponse if auth is false', waitForAsync(() => {
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(false);
|
||||
spyOn(
|
||||
refreshSessionService as any,
|
||||
'startRefreshSession'
|
||||
).and.returnValue(of(null));
|
||||
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
|
||||
false
|
||||
);
|
||||
spyOnProperty(
|
||||
silentRenewService,
|
||||
'refreshSessionWithIFrameCompleted$'
|
||||
).and.returnValue(of(null));
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
|
||||
refreshSessionService
|
||||
.forceRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe((result) => {
|
||||
expect(result).toEqual({
|
||||
isAuthenticated: false,
|
||||
errorMessage: '',
|
||||
userData: null,
|
||||
idToken: '',
|
||||
accessToken: '',
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('occurs timeout error and retry mechanism exhausted max retry count throws error', fakeAsync(() => {
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(false);
|
||||
spyOn(
|
||||
refreshSessionService as any,
|
||||
'startRefreshSession'
|
||||
).and.returnValue(of(null));
|
||||
spyOnProperty(
|
||||
silentRenewService,
|
||||
'refreshSessionWithIFrameCompleted$'
|
||||
).and.returnValue(of(null).pipe(delay(11000)));
|
||||
|
||||
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
|
||||
false
|
||||
);
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
|
||||
const resetSilentRenewRunningSpy = spyOn(
|
||||
flowsDataService,
|
||||
'resetSilentRenewRunning'
|
||||
);
|
||||
const expectedInvokeCount = MAX_RETRY_ATTEMPTS;
|
||||
|
||||
refreshSessionService
|
||||
.forceRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
fail('It should not return any result.');
|
||||
},
|
||||
error: (error) => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(resetSilentRenewRunningSpy).toHaveBeenCalledTimes(
|
||||
expectedInvokeCount
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
tick(allConfigs[0].silentRenewTimeoutInSeconds * 10000);
|
||||
}));
|
||||
|
||||
it('occurs unknown error throws it to subscriber', fakeAsync(() => {
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
|
||||
const expectedErrorMessage = 'Test error message';
|
||||
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(false);
|
||||
spyOnProperty(
|
||||
silentRenewService,
|
||||
'refreshSessionWithIFrameCompleted$'
|
||||
).and.returnValue(of(null));
|
||||
spyOn(
|
||||
refreshSessionService as any,
|
||||
'startRefreshSession'
|
||||
).and.returnValue(throwError(() => new Error(expectedErrorMessage)));
|
||||
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
|
||||
false
|
||||
);
|
||||
|
||||
const resetSilentRenewRunningSpy = spyOn(
|
||||
flowsDataService,
|
||||
'resetSilentRenewRunning'
|
||||
);
|
||||
|
||||
refreshSessionService
|
||||
.forceRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
fail('It should not return any result.');
|
||||
},
|
||||
error: (error) => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.message).toEqual(`Error: ${expectedErrorMessage}`);
|
||||
expect(resetSilentRenewRunningSpy).not.toHaveBeenCalled();
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
describe('NOT isCurrentFlowCodeFlowWithRefreshTokens', () => {
|
||||
it('does return null when not authenticated', waitForAsync(() => {
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(false);
|
||||
spyOn(
|
||||
refreshSessionService as any,
|
||||
'startRefreshSession'
|
||||
).and.returnValue(of(null));
|
||||
spyOn(authStateService, 'areAuthStorageTokensValid').and.returnValue(
|
||||
false
|
||||
);
|
||||
spyOnProperty(
|
||||
silentRenewService,
|
||||
'refreshSessionWithIFrameCompleted$'
|
||||
).and.returnValue(of(null));
|
||||
|
||||
refreshSessionService
|
||||
.forceRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe((result) => {
|
||||
expect(result).toEqual({
|
||||
isAuthenticated: false,
|
||||
errorMessage: '',
|
||||
userData: null,
|
||||
idToken: '',
|
||||
accessToken: '',
|
||||
configId: 'configId1',
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('return value only returns once', waitForAsync(() => {
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
silentRenewTimeoutInSeconds: 10,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(false);
|
||||
spyOn(
|
||||
refreshSessionService as any,
|
||||
'startRefreshSession'
|
||||
).and.returnValue(of(null));
|
||||
spyOnProperty(
|
||||
silentRenewService,
|
||||
'refreshSessionWithIFrameCompleted$'
|
||||
).and.returnValue(
|
||||
of({
|
||||
authResult: {
|
||||
id_token: 'some-id_token',
|
||||
access_token: 'some-access_token',
|
||||
},
|
||||
} as CallbackContext)
|
||||
);
|
||||
const spyInsideMap = spyOn(
|
||||
authStateService,
|
||||
'areAuthStorageTokensValid'
|
||||
).and.returnValue(true);
|
||||
|
||||
refreshSessionService
|
||||
.forceRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe((result) => {
|
||||
expect(result).toEqual({
|
||||
idToken: 'some-id_token',
|
||||
accessToken: 'some-access_token',
|
||||
isAuthenticated: true,
|
||||
userData: undefined,
|
||||
configId: 'configId1',
|
||||
});
|
||||
expect(spyInsideMap).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('startRefreshSession', () => {
|
||||
it('returns null if no auth well known endpoint defined', waitForAsync(() => {
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
|
||||
|
||||
(refreshSessionService as any)
|
||||
.startRefreshSession()
|
||||
.subscribe((result: any) => {
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
}));
|
||||
|
||||
it('returns null if silent renew Is running', waitForAsync(() => {
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(true);
|
||||
|
||||
(refreshSessionService as any)
|
||||
.startRefreshSession()
|
||||
.subscribe((result: any) => {
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls `setSilentRenewRunning` when should be executed', waitForAsync(() => {
|
||||
const setSilentRenewRunningSpy = spyOn(
|
||||
flowsDataService,
|
||||
'setSilentRenewRunning'
|
||||
);
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
authWellknownEndpointUrl: 'https://authWell',
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
|
||||
spyOn(
|
||||
authWellKnownService,
|
||||
'queryAndStoreAuthWellKnownEndPoints'
|
||||
).and.returnValue(of({}));
|
||||
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(true);
|
||||
spyOn(
|
||||
refreshSessionRefreshTokenService,
|
||||
'refreshSessionWithRefreshTokens'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
|
||||
(refreshSessionService as any)
|
||||
.startRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe(() => {
|
||||
expect(setSilentRenewRunningSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls refreshSessionWithRefreshTokens when current flow is codeflow with refresh tokens', waitForAsync(() => {
|
||||
spyOn(flowsDataService, 'setSilentRenewRunning');
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
authWellknownEndpointUrl: 'https://authWell',
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
|
||||
spyOn(
|
||||
authWellKnownService,
|
||||
'queryAndStoreAuthWellKnownEndPoints'
|
||||
).and.returnValue(of({}));
|
||||
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(true);
|
||||
const refreshSessionWithRefreshTokensSpy = spyOn(
|
||||
refreshSessionRefreshTokenService,
|
||||
'refreshSessionWithRefreshTokens'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
|
||||
(refreshSessionService as any)
|
||||
.startRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe(() => {
|
||||
expect(refreshSessionWithRefreshTokensSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('calls refreshSessionWithIframe when current flow is NOT codeflow with refresh tokens', waitForAsync(() => {
|
||||
spyOn(flowsDataService, 'setSilentRenewRunning');
|
||||
const allConfigs = [
|
||||
{
|
||||
configId: 'configId1',
|
||||
authWellknownEndpointUrl: 'https://authWell',
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(flowsDataService, 'isSilentRenewRunning').and.returnValue(false);
|
||||
spyOn(
|
||||
authWellKnownService,
|
||||
'queryAndStoreAuthWellKnownEndPoints'
|
||||
).and.returnValue(of({}));
|
||||
|
||||
spyOn(
|
||||
flowHelper,
|
||||
'isCurrentFlowCodeFlowWithRefreshTokens'
|
||||
).and.returnValue(false);
|
||||
const refreshSessionWithRefreshTokensSpy = spyOn(
|
||||
refreshSessionRefreshTokenService,
|
||||
'refreshSessionWithRefreshTokens'
|
||||
).and.returnValue(of({} as CallbackContext));
|
||||
|
||||
const refreshSessionWithIframeSpy = spyOn(
|
||||
refreshSessionIframeService,
|
||||
'refreshSessionWithIframe'
|
||||
).and.returnValue(of(false));
|
||||
|
||||
(refreshSessionService as any)
|
||||
.startRefreshSession(allConfigs[0], allConfigs)
|
||||
.subscribe(() => {
|
||||
expect(refreshSessionWithRefreshTokensSpy).not.toHaveBeenCalled();
|
||||
expect(refreshSessionWithIframeSpy).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
245
src/callback/refresh-session.service.ts
Normal file
245
src/callback/refresh-session.service.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { inject, Injectable } from 'injection-js';
|
||||
import {
|
||||
forkJoin,
|
||||
Observable,
|
||||
of,
|
||||
throwError,
|
||||
TimeoutError,
|
||||
timer,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
mergeMap,
|
||||
retryWhen,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
timeout,
|
||||
} from 'rxjs/operators';
|
||||
import { AuthStateService } from '../auth-state/auth-state.service';
|
||||
import { AuthWellKnownService } from '../config/auth-well-known/auth-well-known.service';
|
||||
import { OpenIdConfiguration } from '../config/openid-configuration';
|
||||
import { CallbackContext } from '../flows/callback-context';
|
||||
import { FlowsDataService } from '../flows/flows-data.service';
|
||||
import { RefreshSessionIframeService } from '../iframe/refresh-session-iframe.service';
|
||||
import { SilentRenewService } from '../iframe/silent-renew.service';
|
||||
import { LoggerService } from '../logging/logger.service';
|
||||
import { LoginResponse } from '../login/login-response';
|
||||
import { StoragePersistenceService } from '../storage/storage-persistence.service';
|
||||
import { UserService } from '../user-data/user.service';
|
||||
import { FlowHelper } from '../utils/flowHelper/flow-helper.service';
|
||||
import { RefreshSessionRefreshTokenService } from './refresh-session-refresh-token.service';
|
||||
|
||||
export const MAX_RETRY_ATTEMPTS = 3;
|
||||
|
||||
@Injectable()
|
||||
export class RefreshSessionService {
|
||||
private readonly flowHelper = inject(FlowHelper);
|
||||
|
||||
private readonly flowsDataService = inject(FlowsDataService);
|
||||
|
||||
private readonly loggerService = inject(LoggerService);
|
||||
|
||||
private readonly silentRenewService = inject(SilentRenewService);
|
||||
|
||||
private readonly authStateService = inject(AuthStateService);
|
||||
|
||||
private readonly authWellKnownService = inject(AuthWellKnownService);
|
||||
|
||||
private readonly refreshSessionIframeService = inject(
|
||||
RefreshSessionIframeService
|
||||
);
|
||||
|
||||
private readonly storagePersistenceService = inject(
|
||||
StoragePersistenceService
|
||||
);
|
||||
|
||||
private readonly refreshSessionRefreshTokenService = inject(
|
||||
RefreshSessionRefreshTokenService
|
||||
);
|
||||
|
||||
private readonly userService = inject(UserService);
|
||||
|
||||
userForceRefreshSession(
|
||||
config: OpenIdConfiguration | null,
|
||||
allConfigs: OpenIdConfiguration[],
|
||||
extraCustomParams?: { [key: string]: string | number | boolean }
|
||||
): Observable<LoginResponse> {
|
||||
if (!config) {
|
||||
return throwError(
|
||||
() =>
|
||||
new Error(
|
||||
'Please provide a configuration before setting up the module'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.persistCustomParams(extraCustomParams, config);
|
||||
|
||||
return this.forceRefreshSession(config, allConfigs, extraCustomParams).pipe(
|
||||
tap(() => this.flowsDataService.resetSilentRenewRunning(config))
|
||||
);
|
||||
}
|
||||
|
||||
forceRefreshSession(
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[],
|
||||
extraCustomParams?: { [key: string]: string | number | boolean }
|
||||
): Observable<LoginResponse> {
|
||||
const { customParamsRefreshTokenRequest, configId } = config;
|
||||
const mergedParams = {
|
||||
...customParamsRefreshTokenRequest,
|
||||
...extraCustomParams,
|
||||
};
|
||||
|
||||
if (this.flowHelper.isCurrentFlowCodeFlowWithRefreshTokens(config)) {
|
||||
return this.startRefreshSession(config, allConfigs, mergedParams).pipe(
|
||||
map(() => {
|
||||
const isAuthenticated =
|
||||
this.authStateService.areAuthStorageTokensValid(config);
|
||||
|
||||
if (isAuthenticated) {
|
||||
return {
|
||||
idToken: this.authStateService.getIdToken(config),
|
||||
accessToken: this.authStateService.getAccessToken(config),
|
||||
userData: this.userService.getUserDataFromStore(config),
|
||||
isAuthenticated,
|
||||
configId,
|
||||
} as LoginResponse;
|
||||
}
|
||||
|
||||
return {
|
||||
isAuthenticated: false,
|
||||
errorMessage: '',
|
||||
userData: null,
|
||||
idToken: '',
|
||||
accessToken: '',
|
||||
configId,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { silentRenewTimeoutInSeconds } = config;
|
||||
const timeOutTime = (silentRenewTimeoutInSeconds ?? 0) * 1000;
|
||||
|
||||
return forkJoin([
|
||||
this.startRefreshSession(config, allConfigs, extraCustomParams),
|
||||
this.silentRenewService.refreshSessionWithIFrameCompleted$.pipe(take(1)),
|
||||
]).pipe(
|
||||
timeout(timeOutTime),
|
||||
retryWhen((errors) => {
|
||||
return errors.pipe(
|
||||
mergeMap((error, index) => {
|
||||
const scalingDuration = 1000;
|
||||
const currentAttempt = index + 1;
|
||||
|
||||
if (
|
||||
!(error instanceof TimeoutError) ||
|
||||
currentAttempt > MAX_RETRY_ATTEMPTS
|
||||
) {
|
||||
return throwError(() => new Error(error));
|
||||
}
|
||||
|
||||
this.loggerService.logDebug(
|
||||
config,
|
||||
`forceRefreshSession timeout. Attempt #${currentAttempt}`
|
||||
);
|
||||
|
||||
this.flowsDataService.resetSilentRenewRunning(config);
|
||||
|
||||
return timer(currentAttempt * scalingDuration);
|
||||
})
|
||||
);
|
||||
}),
|
||||
map(([_, callbackContext]) => {
|
||||
const isAuthenticated =
|
||||
this.authStateService.areAuthStorageTokensValid(config);
|
||||
|
||||
if (isAuthenticated) {
|
||||
return {
|
||||
idToken: callbackContext?.authResult?.id_token ?? '',
|
||||
accessToken: callbackContext?.authResult?.access_token ?? '',
|
||||
userData: this.userService.getUserDataFromStore(config),
|
||||
isAuthenticated,
|
||||
configId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isAuthenticated: false,
|
||||
errorMessage: '',
|
||||
userData: null,
|
||||
idToken: '',
|
||||
accessToken: '',
|
||||
configId,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private persistCustomParams(
|
||||
extraCustomParams: { [key: string]: string | number | boolean } | undefined,
|
||||
config: OpenIdConfiguration
|
||||
): void {
|
||||
const { useRefreshToken } = config;
|
||||
|
||||
if (extraCustomParams) {
|
||||
if (useRefreshToken) {
|
||||
this.storagePersistenceService.write(
|
||||
'storageCustomParamsRefresh',
|
||||
extraCustomParams,
|
||||
config
|
||||
);
|
||||
} else {
|
||||
this.storagePersistenceService.write(
|
||||
'storageCustomParamsAuthRequest',
|
||||
extraCustomParams,
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private startRefreshSession(
|
||||
config: OpenIdConfiguration,
|
||||
allConfigs: OpenIdConfiguration[],
|
||||
extraCustomParams?: { [key: string]: string | number | boolean }
|
||||
): Observable<boolean | CallbackContext | null> {
|
||||
const isSilentRenewRunning =
|
||||
this.flowsDataService.isSilentRenewRunning(config);
|
||||
|
||||
this.loggerService.logDebug(
|
||||
config,
|
||||
`Checking: silentRenewRunning: ${isSilentRenewRunning}`
|
||||
);
|
||||
const shouldBeExecuted = !isSilentRenewRunning;
|
||||
|
||||
if (!shouldBeExecuted) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.authWellKnownService
|
||||
.queryAndStoreAuthWellKnownEndPoints(config)
|
||||
.pipe(
|
||||
switchMap(() => {
|
||||
this.flowsDataService.setSilentRenewRunning(config);
|
||||
|
||||
if (this.flowHelper.isCurrentFlowCodeFlowWithRefreshTokens(config)) {
|
||||
// Refresh Session using Refresh tokens
|
||||
return this.refreshSessionRefreshTokenService.refreshSessionWithRefreshTokens(
|
||||
config,
|
||||
allConfigs,
|
||||
extraCustomParams
|
||||
);
|
||||
}
|
||||
|
||||
return this.refreshSessionIframeService.refreshSessionWithIframe(
|
||||
config,
|
||||
allConfigs,
|
||||
extraCustomParams
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user