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

This commit is contained in:
2025-01-18 01:05:00 +08:00
parent 276d9fbda8
commit 983254164b
201 changed files with 35689 additions and 0 deletions

View 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();
});
}));
});
});

View 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()));
}
}

View 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');
},
});
}));
});
});

View 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));
})
);
}
}

View 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');
},
});
}));
});
});

View 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));
})
);
}
}

View 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();
}));
});
});

View 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);
};
});
}
}

View 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();
});
});
});

View 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;
}
}

View 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();
}));
});
});

View 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()
)
);
}
}

View 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();
});
}));
});
});

View 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
);
})
);
}
}