oidc-client-rx/src/callback/periodically-token-check.service.ts
2025-02-02 00:45:46 +08:00

269 lines
8.7 KiB
TypeScript

import { Injectable, inject } from 'injection-js';
import { type Observable, ReplaySubject, forkJoin, of, throwError } from 'rxjs';
import { catchError, map, share, switchMap } from 'rxjs/operators';
import { AuthStateService } from '../auth-state/auth-state.service';
import { ConfigurationService } from '../config/config.service';
import type { OpenIdConfiguration } from '../config/openid-configuration';
import type { 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
): Observable<undefined> {
const configsWithSilentRenewEnabled =
this.getConfigsWithSilentRenewEnabled(allConfigs);
if (configsWithSilentRenewEnabled.length <= 0) {
return of(undefined);
}
if (this.intervalService.isTokenValidationRunning()) {
return of(undefined);
}
const refreshTimeInSeconds = this.getSmallestRefreshTimeFromConfigs(
configsWithSilentRenewEnabled
);
const periodicallyCheck$ = this.intervalService
.startPeriodicTokenCheck(refreshTimeInSeconds)
.pipe(
switchMap(() => {
const objectWithConfigIdsAndRefreshEvent: {
[id: string]: Observable<boolean | CallbackContext | null>;
} = {};
for (const config of configsWithSilentRenewEnabled) {
const identifier = config.configId as string;
const refreshEvent = this.getRefreshEvent(config, allConfigs);
objectWithConfigIdsAndRefreshEvent[identifier] = refreshEvent;
}
return forkJoin(objectWithConfigIdsAndRefreshEvent);
})
);
const o$ = periodicallyCheck$.pipe(
catchError((error) => {
this.loggerService.logError(
currentConfig,
'silent renew failed!',
error
);
return throwError(() => error);
}),
map((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);
}
});
return undefined;
}
}),
share({
connector: () => new ReplaySubject(1),
resetOnError: false,
resetOnComplete: false,
resetOnRefCountZero: false,
})
);
this.intervalService.runTokenValidationRunning = o$.subscribe({});
return o$;
}
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;
}
}