AAE-29442 moving to node 20.18.1 (#10500)

* AAE-0000 - moving to node 20.18.1

* AAE-29442 Adjusted to the new eslint rule
This commit is contained in:
Vito Albano
2024-12-18 11:14:52 +00:00
committed by GitHub
parent dd44e492b7
commit 872fb16b62
144 changed files with 5267 additions and 6842 deletions

View File

@@ -10,7 +10,7 @@
"createDefaultProgram": true
},
"rules": {
"jsdoc/newline-after-description": "warn",
"jsdoc/tag-lines": ["error", "any", {"startLines": 1}],
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/consistent-type-assertions": "warn",
"@typescript-eslint/prefer-for-of": "off",

View File

@@ -25,7 +25,6 @@ export class DebugAppConfigService extends AppConfigService {
super();
}
/** @override */
get<T>(key: string, defaultValue?: T): T {
if (key === AppConfigValues.OAUTHCONFIG) {
return JSON.parse(this.storage.getItem(key)) || super.get<T>(key, defaultValue);

View File

@@ -254,6 +254,8 @@ export class BasicAlfrescoAuthService extends BaseAuthenticationService {
/**
* logout Alfresco API
*
* @returns A promise that returns {logout} if resolved and {error} if rejected.
*/
async logout(): Promise<any> {
if (this.isBPMProvider()) {

View File

@@ -22,57 +22,53 @@ import { Observable } from 'rxjs';
* Provide authentication/authorization through OAuth2/OIDC protocol.
*/
export abstract class AuthService {
abstract onLogin: Observable<any>;
abstract onLogin: Observable<any>;
/**
* An observable that emits a value when a logout event occurs.
* Implement this observable to handle any necessary cleanup or state updates
* when a user logs out of the application.
*
* @type {Observable<void>}
*/
abstract onLogout$: Observable<void>;
/**
* An observable that emits a value when a logout event occurs.
* Implement this observable to handle any necessary cleanup or state updates
* when a user logs out of the application.
*/
abstract onLogout$: Observable<void>;
abstract onTokenReceived: Observable<any>;
abstract onTokenReceived: Observable<any>;
/**
* An abstract observable that emits a boolean value indicating whether the discovery document
* has been successfully loaded.
*
* @type {Observable<boolean>}
*/
abstract isDiscoveryDocumentLoaded$: Observable<boolean>;
/**
* An abstract observable that emits a boolean value indicating whether the discovery document
* has been successfully loaded.
*/
abstract isDiscoveryDocumentLoaded$: Observable<boolean>;
/** Subscribe to whether the user has valid Id/Access tokens. */
abstract authenticated$: Observable<boolean>;
/** Subscribe to whether the user has valid Id/Access tokens. */
abstract authenticated$: Observable<boolean>;
/** Get whether the user has valid Id/Access tokens. */
abstract authenticated: boolean;
/** Get whether the user has valid Id/Access tokens. */
abstract authenticated: boolean;
/** Subscribe to errors reaching the IdP. */
abstract idpUnreachable$: Observable<Error>;
/** Subscribe to errors reaching the IdP. */
abstract idpUnreachable$: Observable<Error>;
/**
* Initiate the IdP login flow.
*/
abstract login(currentUrl?: string): Promise<void> | void;
/**
* Initiate the IdP login flow.
*/
abstract login(currentUrl?: string): Promise<void> | void;
abstract baseAuthLogin(username: string, password: string): Observable<TokenResponse> ;
abstract baseAuthLogin(username: string, password: string): Observable<TokenResponse>;
/**
* Disconnect from IdP.
*
* @returns Promise may be returned depending on implementation
*/
abstract logout(): Promise<void> | void;
/**
* Disconnect from IdP.
*
* @returns Promise may be returned depending on implementation
*/
abstract logout(): Promise<void> | void;
/**
* Complete the login flow.
*
* In browsers, checks URL for auth and stored state. Call this once the application returns from IdP.
*
* @returns Promise, resolve with stored state, reject if unable to reach IdP
*/
abstract loginCallback(loginOptions?: LoginOptions): Promise<string | undefined>;
abstract updateIDPConfiguration(...args: any[]): void;
/**
* Complete the login flow.
*
* In browsers, checks URL for auth and stored state. Call this once the application returns from IdP.
*
* @returns Promise, resolve with stored state, reject if unable to reach IdP
*/
abstract loginCallback(loginOptions?: LoginOptions): Promise<string | undefined>;
abstract updateIDPConfiguration(...args: any[]): void;
}

View File

@@ -51,8 +51,6 @@ export class OidcAuthenticationService extends BaseAuthenticationService {
* This observable combines the authentication status and the discovery document load status
* to decide if an SSO login is necessary. It emits `true` if the user is not authenticated
* and the discovery document is loaded, otherwise it emits `false`.
*
* @type {Observable<boolean>}
*/
shouldPerformSsoLogin$: Observable<boolean> = combineLatest([this.auth.authenticated$, this.auth.isDiscoveryDocumentLoaded$]).pipe(
map(([authenticated, isDiscoveryDocumentLoaded]) => !authenticated && isDiscoveryDocumentLoaded)

View File

@@ -16,7 +16,18 @@
*/
import { Inject, Injectable, inject } from '@angular/core';
import { AuthConfig, AUTH_CONFIG, OAuthErrorEvent, OAuthEvent, OAuthService, OAuthStorage, TokenResponse, LoginOptions, OAuthSuccessEvent, OAuthLogger } from 'angular-oauth2-oidc';
import {
AuthConfig,
AUTH_CONFIG,
OAuthErrorEvent,
OAuthEvent,
OAuthService,
OAuthStorage,
TokenResponse,
LoginOptions,
OAuthSuccessEvent,
OAuthLogger
} from 'angular-oauth2-oidc';
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
import { from, Observable, race, ReplaySubject } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay, switchMap, take } from 'rxjs/operators';
@@ -29,394 +40,400 @@ const isPromise = <T>(value: T | Promise<T>): value is Promise<T> => value && ty
@Injectable()
export class RedirectAuthService extends AuthService {
readonly authModuleConfig: AuthModuleConfig = inject(AUTH_MODULE_CONFIG);
private readonly _retryLoginService: RetryLoginService = inject(RetryLoginService);
private readonly _oauthLogger: OAuthLogger = inject(OAuthLogger);
private readonly _timeSyncService: TimeSyncService = inject(TimeSyncService);
readonly authModuleConfig: AuthModuleConfig = inject(AUTH_MODULE_CONFIG);
private readonly _retryLoginService: RetryLoginService = inject(RetryLoginService);
private readonly _oauthLogger: OAuthLogger = inject(OAuthLogger);
private readonly _timeSyncService: TimeSyncService = inject(TimeSyncService);
private _isDiscoveryDocumentLoadedSubject$ = new ReplaySubject<boolean>();
public isDiscoveryDocumentLoaded$ = this._isDiscoveryDocumentLoadedSubject$.asObservable();
private _isDiscoveryDocumentLoadedSubject$ = new ReplaySubject<boolean>();
public isDiscoveryDocumentLoaded$ = this._isDiscoveryDocumentLoadedSubject$.asObservable();
onLogin: Observable<any>;
onLogin: Observable<any>;
onTokenReceived: Observable<any>;
onTokenReceived: Observable<any>;
private _loadDiscoveryDocumentPromise = Promise.resolve(false);
private _loadDiscoveryDocumentPromise = Promise.resolve(false);
/**
* Observable stream that emits when the user logs out.
*
* This observable listens to the events emitted by the OAuth service and filters
* them to only include instances of OAuthSuccessEvent with the type `logout`.
*/
onLogout$: Observable<void>;
/**
* Observable stream that emits when the user logs out.
*
* This observable listens to the events emitted by the OAuth service and filters
* them to only include instances of OAuthSuccessEvent with the type `logout`.
*
* @type {Observable<void>}
*/
onLogout$: Observable<void>;
/**
* Observable stream that emits OAuthErrorEvent instances.
*
* This observable listens to the events emitted by the OAuth service and filters
* them to only include instances of OAuthErrorEvent. It then maps these events
* to the correct type.
*/
oauthErrorEvent$: Observable<OAuthErrorEvent>;
/**
* Observable stream that emits OAuthErrorEvent instances.
*
* This observable listens to the events emitted by the OAuth service and filters
* them to only include instances of OAuthErrorEvent. It then maps these events
* to the correct type.
*
* @type {Observable<OAuthErrorEvent>}
*/
oauthErrorEvent$: Observable<OAuthErrorEvent>;
/**
* Observable stream that emits the first OAuth error event that occurs.
*/
firstOauthErrorEventOccur$: Observable<OAuthErrorEvent>;
/**
* Observable stream that emits the first OAuth error event that occurs.
*/
firstOauthErrorEventOccur$: Observable<OAuthErrorEvent>;
/**
* Observable stream that emits the first OAuth error event that occurs, excluding token refresh errors.
*/
firstOauthErrorEventExcludingTokenRefreshError$: Observable<OAuthErrorEvent>;
/**
* Observable stream that emits the first OAuth error event that occurs, excluding token refresh errors.
*/
firstOauthErrorEventExcludingTokenRefreshError$: Observable<OAuthErrorEvent>;
/**
* Observable stream that emits the second OAuth token refresh error event that occurs.
*/
secondTokenRefreshErrorEventOccur$: Observable<OAuthErrorEvent>;
/**
* Observable stream that emits the second OAuth token refresh error event that occurs.
*/
secondTokenRefreshErrorEventOccur$: Observable<OAuthErrorEvent>;
/**
* Observable that emits an error when the token has expired due to
* the local machine clock being out of sync with the server time.
*/
tokenHasExpiredDueToClockOutOfSync$: Observable<Error>;
/**
* Observable that emits an error when the token has expired due to
* the local machine clock being out of sync with the server time.
*
* @type {Observable<Error>}
*/
tokenHasExpiredDueToClockOutOfSync$: Observable<Error>;
/**
* Observable that emits an error when the OAuth error event occurs due to
* the local machine clock being out of sync with the server time.
*/
oauthErrorEventOccurDueToClockOutOfSync$: Observable<Error>;
/**
* Observable that emits an error when the OAuth error event occurs due to
* the local machine clock being out of sync with the server time.
*
* @type {Observable<Error>}
*/
oauthErrorEventOccurDueToClockOutOfSync$: Observable<Error>;
/**
* Observable stream that emits either OAuthErrorEvent or Error.
* This stream combines multiple OAuth error sources into a single observable.
*/
combinedOAuthErrorsStream$: Observable<OAuthErrorEvent | Error>;
/**
* Observable stream that emits either OAuthErrorEvent or Error.
* This stream combines multiple OAuth error sources into a single observable.
*/
combinedOAuthErrorsStream$: Observable<OAuthErrorEvent | Error>;
/** Subscribe to whether the user has valid Id/Access tokens. */
authenticated$!: Observable<boolean>;
/** Subscribe to whether the user has valid Id/Access tokens. */
authenticated$!: Observable<boolean>;
/** Subscribe to errors reaching the IdP. */
idpUnreachable$!: Observable<Error>;
/**
* Get whether the user has valid Id/Access tokens.
*
* @returns `true` if the user is authenticated, otherwise `false`
*/
get authenticated(): boolean {
return this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken();
}
private authConfig!: AuthConfig | Promise<AuthConfig>;
private readonly AUTH_STORAGE_ITEMS: string[] = [
'access_token',
'access_token_stored_at',
'expires_at',
'granted_scopes',
'id_token',
'id_token_claims_obj',
'id_token_expires_at',
'id_token_stored_at',
'nonce',
'PKCE_verifier',
'refresh_token',
'session_state'
];
constructor(
private oauthService: OAuthService,
private _oauthStorage: OAuthStorage,
@Inject(AUTH_CONFIG) authConfig: AuthConfig
) {
super();
this.authConfig = authConfig;
this.oauthService.clearHashAfterLogin = true;
this.oauthService.events.pipe(
filter(() => oauthService.showDebugInformation))
.subscribe(event => {
if (event instanceof OAuthErrorEvent) {
this._oauthLogger.error('OAuthErrorEvent Object:', event);
} else {
this._oauthLogger.info('OAuthEvent Object:', event);
}
});
this.oauthErrorEvent$ = this.oauthService.events.pipe(
filter(event => event instanceof OAuthErrorEvent),
map((event) => event as OAuthErrorEvent)
);
this.firstOauthErrorEventOccur$ = this.oauthErrorEvent$.pipe(take(1));
this.firstOauthErrorEventExcludingTokenRefreshError$ = this.oauthErrorEvent$.pipe(
filter(event => event instanceof OAuthErrorEvent && event.type !== 'token_refresh_error'),
take(1)
);
this.secondTokenRefreshErrorEventOccur$ = this.oauthErrorEvent$.pipe(
filter(event => event.type === 'token_refresh_error'),
take(2),
filter((_, index) => index === 1)
);
this.oauthErrorEventOccurDueToClockOutOfSync$ = this.oauthErrorEvent$.pipe(
switchMap(() => this._timeSyncService.checkTimeSync(this.oauthService.clockSkewInSec)),
filter((timeSync) => timeSync?.outOfSync),
map((timeSync) => new Error(`OAuth error occurred due to local machine clock ${timeSync.localDateTimeISO} being out of sync with server time ${timeSync.serverDateTimeISO}`)),
take(1)
);
this.authenticated$ = this.oauthService.events.pipe(
map(() => this.authenticated),
distinctUntilChanged(),
shareReplay(1)
);
this.tokenHasExpiredDueToClockOutOfSync$ = this.oauthService.events.pipe(
map(() => !!this.oauthService.getIdentityClaims() && this.tokenHasExpired()),
filter((hasExpired) => hasExpired),
switchMap(() => this._timeSyncService.checkTimeSync(this.oauthService.clockSkewInSec)),
filter((timeSync) => timeSync?.outOfSync),
map((timeSync) => new Error(`Token has expired due to local machine clock ${timeSync.localDateTimeISO} being out of sync with server time ${timeSync.serverDateTimeISO}`)),
take(1)
);
this.onLogout$ = this.oauthService.events.pipe(
filter((event) => event.type === 'logout'),
map(() => undefined)
);
this.combinedOAuthErrorsStream$ = race([
this.oauthErrorEventOccurDueToClockOutOfSync$,
this.firstOauthErrorEventExcludingTokenRefreshError$,
this.tokenHasExpiredDueToClockOutOfSync$,
this.secondTokenRefreshErrorEventOccur$
]);
this.combinedOAuthErrorsStream$.subscribe({
next: (res) => {
this._oauthLogger.error(res);
this.logout();
},
error: () => {}
});
this.oauthService.events.pipe(take(1)).subscribe(() => {
if(this.oauthService.getAccessToken() && !this.oauthService.hasValidAccessToken()) {
if(this.oauthService.showDebugInformation) {
this._oauthLogger.warn('Access token not valid. Removing all auth items from storage');
}
this.AUTH_STORAGE_ITEMS.map((item: string) => this._oauthStorage.removeItem(item));
}
});
this.onLogin = this.authenticated$.pipe(
filter((authenticated) => authenticated),
map(() => undefined)
);
this.onTokenReceived = this.oauthService.events.pipe(
filter((event: OAuthEvent) => event.type === 'token_received'),
map(() => undefined)
);
this.idpUnreachable$ = this.oauthService.events.pipe(
filter((event): event is OAuthErrorEvent => event.type === 'discovery_document_load_error'),
map((event) => event.reason as Error)
);
/** Subscribe to errors reaching the IdP. */
idpUnreachable$!: Observable<Error>;
/**
* Get whether the user has valid Id/Access tokens.
*
* @returns `true` if the user is authenticated, otherwise `false`
*/
get authenticated(): boolean {
return this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken();
}
init(): Promise<boolean> {
if (isPromise(this.authConfig)) {
return this.authConfig.then((config) => this.configureAuth(config));
}
private authConfig!: AuthConfig | Promise<AuthConfig>;
return this.configureAuth(this.authConfig);
}
private readonly AUTH_STORAGE_ITEMS: string[] = [
'access_token',
'access_token_stored_at',
'expires_at',
'granted_scopes',
'id_token',
'id_token_claims_obj',
'id_token_expires_at',
'id_token_stored_at',
'nonce',
'PKCE_verifier',
'refresh_token',
'session_state'
];
logout() {
this.oauthService.logOut();
}
constructor(private oauthService: OAuthService, private _oauthStorage: OAuthStorage, @Inject(AUTH_CONFIG) authConfig: AuthConfig) {
super();
ensureDiscoveryDocument(): Promise<boolean> {
this._loadDiscoveryDocumentPromise = this._loadDiscoveryDocumentPromise
.catch(() => false)
.then((loaded) => {
if (!loaded) {
return this.oauthService.loadDiscoveryDocument().then(() => true);
}
return true;
});
return this._loadDiscoveryDocumentPromise;
}
this.authConfig = authConfig;
this.oauthService.clearHashAfterLogin = true;
login(currentUrl?: string): void {
let stateKey: string | undefined;
if (currentUrl) {
const randomValue = window.crypto.getRandomValues(new Uint32Array(1))[0];
stateKey = `auth_state_${randomValue}${Date.now()}`;
this._oauthStorage.setItem(stateKey, JSON.stringify(currentUrl || {}));
}
// initLoginFlow will initialize the login flow in either code or implicit depending on the configuration
this.ensureDiscoveryDocument().then(() => void this.oauthService.initLoginFlow(stateKey));
}
baseAuthLogin(username: string, password: string): Observable<TokenResponse> {
this.oauthService.useHttpBasicAuth = true;
return from(this.oauthService.fetchTokenUsingPasswordFlow(username, password)).pipe(
map((response) => {
const props = new Map<string, string>();
props.set('id_token', response.id_token);
// for backward compatibility we need to set the response in our storage
this.oauthService['storeAccessTokenResponse'](response.access_token, response.refresh_token, response.expires_in, response.scope, props);
return response;
})
);
}
async loginCallback(loginOptions?: LoginOptions): Promise<string | undefined> {
return this.ensureDiscoveryDocument()
.then(() => this._retryLoginService.tryToLoginTimes({ ...loginOptions, preventClearHashAfterLogin: this.authModuleConfig.preventClearHashAfterLogin }))
.then(() => this._getRedirectUrl());
}
private _getRedirectUrl() {
const DEFAULT_REDIRECT = '/';
const stateKey = this.oauthService.state;
if (stateKey) {
const stateStringified = this._oauthStorage.getItem(stateKey);
if (stateStringified) {
// cleanup state from storage
this._oauthStorage.removeItem(stateKey);
return JSON.parse(stateStringified);
}
}
return DEFAULT_REDIRECT;
}
private configureAuth(config: AuthConfig): Promise<boolean> {
this.oauthService.configure(config);
this.oauthService.tokenValidationHandler = new JwksValidationHandler();
if (config.sessionChecksEnabled) {
this.oauthService.events.pipe(filter((event) => event.type === 'session_terminated')).subscribe(() => {
this.oauthService.logOut();
});
}
return this.ensureDiscoveryDocument().then(() => {
this._isDiscoveryDocumentLoadedSubject$.next(true);
this.oauthService.setupAutomaticSilentRefresh();
return void this.allowRefreshTokenAndSilentRefreshOnMultipleTabs();
}).catch(() => {
// catch error to prevent the app from crashing when trying to access unprotected routes
});
}
/**
* Fix a known issue (https://github.com/manfredsteyer/angular-oauth2-oidc/issues/850)
* where multiple tabs can cause the token refresh and the silent refresh to fail.
* This patch is based on the solutions provided in the following comments:
* https://github.com/manfredsteyer/angular-oauth2-oidc/issues/850#issuecomment-889921776 fix silent refresh for the implicit flow
* https://github.com/manfredsteyer/angular-oauth2-oidc/issues/850#issuecomment-1557286966 fix refresh token for the code flow
*/
private allowRefreshTokenAndSilentRefreshOnMultipleTabs() {
let lastUpdatedAccessToken: string | undefined;
if (this.oauthService.hasValidAccessToken()) {
lastUpdatedAccessToken = this.oauthService.getAccessToken();
}
const originalRefreshToken = this.oauthService.refreshToken.bind(this.oauthService);
this.oauthService.refreshToken = (): Promise<TokenResponse> =>
navigator.locks.request(`refresh_tokens_${location.origin}`, () => {
if (!!lastUpdatedAccessToken && lastUpdatedAccessToken !== this.oauthService.getAccessToken()) {
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_received'));
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_refreshed'));
lastUpdatedAccessToken = this.oauthService.getAccessToken();
return;
}
return originalRefreshToken().then((resp) => (lastUpdatedAccessToken = resp.access_token));
});
const originalSilentRefresh = this.oauthService.silentRefresh.bind(this.oauthService);
this.oauthService.silentRefresh = async (params: any = {}, noPrompt = true): Promise<OAuthEvent> =>
navigator.locks.request(`silent_refresh_${location.origin}`, async (): Promise<OAuthEvent> => {
if (lastUpdatedAccessToken !== this.oauthService.getAccessToken()) {
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_received'));
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_refreshed'));
const event = new OAuthSuccessEvent('silently_refreshed');
(this.oauthService as any).eventsSubject.next(event);
lastUpdatedAccessToken = this.oauthService.getAccessToken();
return event;
this.oauthService.events.pipe(filter(() => oauthService.showDebugInformation)).subscribe((event) => {
if (event instanceof OAuthErrorEvent) {
this._oauthLogger.error('OAuthErrorEvent Object:', event);
} else {
return originalSilentRefresh(params, noPrompt);
this._oauthLogger.info('OAuthEvent Object:', event);
}
});
}
updateIDPConfiguration(config: AuthConfig) {
this.oauthService.configure(config);
}
this.oauthErrorEvent$ = this.oauthService.events.pipe(
filter((event) => event instanceof OAuthErrorEvent),
map((event) => event as OAuthErrorEvent)
);
this.firstOauthErrorEventOccur$ = this.oauthErrorEvent$.pipe(take(1));
/**
* Checks if the token has expired.
*
* This method retrieves the identity claims from the OAuth service and calculates
* the token's issued and expiration times. It then compares the current time with
* these values, considering a clock skew and a configurable expiration decrease.
*
* @returns - Returns `true` if the token has expired, otherwise `false`.
*/
tokenHasExpired(){
const claims = this.oauthService.getIdentityClaims();
if(!claims){
this._oauthLogger.warn('No claims found in the token');
return false;
this.firstOauthErrorEventExcludingTokenRefreshError$ = this.oauthErrorEvent$.pipe(
filter((event) => event instanceof OAuthErrorEvent && event.type !== 'token_refresh_error'),
take(1)
);
this.secondTokenRefreshErrorEventOccur$ = this.oauthErrorEvent$.pipe(
filter((event) => event.type === 'token_refresh_error'),
take(2),
filter((_, index) => index === 1)
);
this.oauthErrorEventOccurDueToClockOutOfSync$ = this.oauthErrorEvent$.pipe(
switchMap(() => this._timeSyncService.checkTimeSync(this.oauthService.clockSkewInSec)),
filter((timeSync) => timeSync?.outOfSync),
map(
(timeSync) =>
new Error(
`OAuth error occurred due to local machine clock ${timeSync.localDateTimeISO} being out of sync with server time ${timeSync.serverDateTimeISO}`
)
),
take(1)
);
this.authenticated$ = this.oauthService.events.pipe(
map(() => this.authenticated),
distinctUntilChanged(),
shareReplay(1)
);
this.tokenHasExpiredDueToClockOutOfSync$ = this.oauthService.events.pipe(
map(() => !!this.oauthService.getIdentityClaims() && this.tokenHasExpired()),
filter((hasExpired) => hasExpired),
switchMap(() => this._timeSyncService.checkTimeSync(this.oauthService.clockSkewInSec)),
filter((timeSync) => timeSync?.outOfSync),
map(
(timeSync) =>
new Error(
`Token has expired due to local machine clock ${timeSync.localDateTimeISO} being out of sync with server time ${timeSync.serverDateTimeISO}`
)
),
take(1)
);
this.onLogout$ = this.oauthService.events.pipe(
filter((event) => event.type === 'logout'),
map(() => undefined)
);
this.combinedOAuthErrorsStream$ = race([
this.oauthErrorEventOccurDueToClockOutOfSync$,
this.firstOauthErrorEventExcludingTokenRefreshError$,
this.tokenHasExpiredDueToClockOutOfSync$,
this.secondTokenRefreshErrorEventOccur$
]);
this.combinedOAuthErrorsStream$.subscribe({
next: (res) => {
this._oauthLogger.error(res);
this.logout();
},
error: () => {}
});
this.oauthService.events.pipe(take(1)).subscribe(() => {
if (this.oauthService.getAccessToken() && !this.oauthService.hasValidAccessToken()) {
if (this.oauthService.showDebugInformation) {
this._oauthLogger.warn('Access token not valid. Removing all auth items from storage');
}
this.AUTH_STORAGE_ITEMS.map((item: string) => this._oauthStorage.removeItem(item));
}
});
this.onLogin = this.authenticated$.pipe(
filter((authenticated) => authenticated),
map(() => undefined)
);
this.onTokenReceived = this.oauthService.events.pipe(
filter((event: OAuthEvent) => event.type === 'token_received'),
map(() => undefined)
);
this.idpUnreachable$ = this.oauthService.events.pipe(
filter((event): event is OAuthErrorEvent => event.type === 'discovery_document_load_error'),
map((event) => event.reason as Error)
);
}
const now = Date.now();
const issuedAtMSec = claims.iat * 1000;
const expiresAtMSec = claims.exp * 1000;
const clockSkewInMSec = this.oauthService.clockSkewInSec * 1000;
this.showTokenExpiredDebugInformations(now, issuedAtMSec, expiresAtMSec, clockSkewInMSec);
return issuedAtMSec - clockSkewInMSec >= now ||
expiresAtMSec + clockSkewInMSec - this.oauthService.decreaseExpirationBySec <= now;
}
init(): Promise<boolean> {
if (isPromise(this.authConfig)) {
return this.authConfig.then((config) => this.configureAuth(config));
}
private showTokenExpiredDebugInformations(now: number, issuedAtMSec: number, expiresAtMSec: number, clockSkewInMSec: number) {
if(this.oauthService.showDebugInformation) {
this._oauthLogger.warn('now: ', new Date(now));
this._oauthLogger.warn('issuedAt: ', new Date(issuedAtMSec));
this._oauthLogger.warn('expiresAt: ', new Date(expiresAtMSec));
this._oauthLogger.warn('clockSkewInMSec: ', clockSkewInMSec);
this._oauthLogger.warn('this.oauthService.decreaseExpirationBySec: ', this.oauthService.decreaseExpirationBySec);
this._oauthLogger.warn('issuedAtMSec - clockSkewInMSec >= now: ', issuedAtMSec - clockSkewInMSec >= now);
this._oauthLogger.warn('expiresAtMSec + clockSkewInMSec - this.oauthService.decreaseExpirationBySec <= now: ', expiresAtMSec + clockSkewInMSec - this.oauthService.decreaseExpirationBySec <= now);
return this.configureAuth(this.authConfig);
}
}
logout() {
this.oauthService.logOut();
}
ensureDiscoveryDocument(): Promise<boolean> {
this._loadDiscoveryDocumentPromise = this._loadDiscoveryDocumentPromise
.catch(() => false)
.then((loaded) => {
if (!loaded) {
return this.oauthService.loadDiscoveryDocument().then(() => true);
}
return true;
});
return this._loadDiscoveryDocumentPromise;
}
login(currentUrl?: string): void {
let stateKey: string | undefined;
if (currentUrl) {
const randomValue = window.crypto.getRandomValues(new Uint32Array(1))[0];
stateKey = `auth_state_${randomValue}${Date.now()}`;
this._oauthStorage.setItem(stateKey, JSON.stringify(currentUrl || {}));
}
// initLoginFlow will initialize the login flow in either code or implicit depending on the configuration
this.ensureDiscoveryDocument().then(() => void this.oauthService.initLoginFlow(stateKey));
}
baseAuthLogin(username: string, password: string): Observable<TokenResponse> {
this.oauthService.useHttpBasicAuth = true;
return from(this.oauthService.fetchTokenUsingPasswordFlow(username, password)).pipe(
map((response) => {
const props = new Map<string, string>();
props.set('id_token', response.id_token);
// for backward compatibility we need to set the response in our storage
this.oauthService['storeAccessTokenResponse'](
response.access_token,
response.refresh_token,
response.expires_in,
response.scope,
props
);
return response;
})
);
}
async loginCallback(loginOptions?: LoginOptions): Promise<string | undefined> {
return this.ensureDiscoveryDocument()
.then(() =>
this._retryLoginService.tryToLoginTimes({
...loginOptions,
preventClearHashAfterLogin: this.authModuleConfig.preventClearHashAfterLogin
})
)
.then(() => this._getRedirectUrl());
}
private _getRedirectUrl() {
const DEFAULT_REDIRECT = '/';
const stateKey = this.oauthService.state;
if (stateKey) {
const stateStringified = this._oauthStorage.getItem(stateKey);
if (stateStringified) {
// cleanup state from storage
this._oauthStorage.removeItem(stateKey);
return JSON.parse(stateStringified);
}
}
return DEFAULT_REDIRECT;
}
private configureAuth(config: AuthConfig): Promise<boolean> {
this.oauthService.configure(config);
this.oauthService.tokenValidationHandler = new JwksValidationHandler();
if (config.sessionChecksEnabled) {
this.oauthService.events.pipe(filter((event) => event.type === 'session_terminated')).subscribe(() => {
this.oauthService.logOut();
});
}
return this.ensureDiscoveryDocument()
.then(() => {
this._isDiscoveryDocumentLoadedSubject$.next(true);
this.oauthService.setupAutomaticSilentRefresh();
return void this.allowRefreshTokenAndSilentRefreshOnMultipleTabs();
})
.catch(() => {
// catch error to prevent the app from crashing when trying to access unprotected routes
});
}
/**
* Fix a known issue (https://github.com/manfredsteyer/angular-oauth2-oidc/issues/850)
* where multiple tabs can cause the token refresh and the silent refresh to fail.
* This patch is based on the solutions provided in the following comments:
* https://github.com/manfredsteyer/angular-oauth2-oidc/issues/850#issuecomment-889921776 fix silent refresh for the implicit flow
* https://github.com/manfredsteyer/angular-oauth2-oidc/issues/850#issuecomment-1557286966 fix refresh token for the code flow
*/
private allowRefreshTokenAndSilentRefreshOnMultipleTabs() {
let lastUpdatedAccessToken: string | undefined;
if (this.oauthService.hasValidAccessToken()) {
lastUpdatedAccessToken = this.oauthService.getAccessToken();
}
const originalRefreshToken = this.oauthService.refreshToken.bind(this.oauthService);
this.oauthService.refreshToken = (): Promise<TokenResponse> =>
navigator.locks.request(`refresh_tokens_${location.origin}`, () => {
if (!!lastUpdatedAccessToken && lastUpdatedAccessToken !== this.oauthService.getAccessToken()) {
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_received'));
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_refreshed'));
lastUpdatedAccessToken = this.oauthService.getAccessToken();
return;
}
return originalRefreshToken().then((resp) => (lastUpdatedAccessToken = resp.access_token));
});
const originalSilentRefresh = this.oauthService.silentRefresh.bind(this.oauthService);
this.oauthService.silentRefresh = async (params: any = {}, noPrompt = true): Promise<OAuthEvent> =>
navigator.locks.request(`silent_refresh_${location.origin}`, async (): Promise<OAuthEvent> => {
if (lastUpdatedAccessToken !== this.oauthService.getAccessToken()) {
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_received'));
(this.oauthService as any).eventsSubject.next(new OAuthSuccessEvent('token_refreshed'));
const event = new OAuthSuccessEvent('silently_refreshed');
(this.oauthService as any).eventsSubject.next(event);
lastUpdatedAccessToken = this.oauthService.getAccessToken();
return event;
} else {
return originalSilentRefresh(params, noPrompt);
}
});
}
updateIDPConfiguration(config: AuthConfig) {
this.oauthService.configure(config);
}
/**
* Checks if the token has expired.
*
* This method retrieves the identity claims from the OAuth service and calculates
* the token's issued and expiration times. It then compares the current time with
* these values, considering a clock skew and a configurable expiration decrease.
*
* @returns - Returns `true` if the token has expired, otherwise `false`.
*/
tokenHasExpired() {
const claims = this.oauthService.getIdentityClaims();
if (!claims) {
this._oauthLogger.warn('No claims found in the token');
return false;
}
const now = Date.now();
const issuedAtMSec = claims.iat * 1000;
const expiresAtMSec = claims.exp * 1000;
const clockSkewInMSec = this.oauthService.clockSkewInSec * 1000;
this.showTokenExpiredDebugInformations(now, issuedAtMSec, expiresAtMSec, clockSkewInMSec);
return issuedAtMSec - clockSkewInMSec >= now || expiresAtMSec + clockSkewInMSec - this.oauthService.decreaseExpirationBySec <= now;
}
private showTokenExpiredDebugInformations(now: number, issuedAtMSec: number, expiresAtMSec: number, clockSkewInMSec: number) {
if (this.oauthService.showDebugInformation) {
this._oauthLogger.warn('now: ', new Date(now));
this._oauthLogger.warn('issuedAt: ', new Date(issuedAtMSec));
this._oauthLogger.warn('expiresAt: ', new Date(expiresAtMSec));
this._oauthLogger.warn('clockSkewInMSec: ', clockSkewInMSec);
this._oauthLogger.warn('this.oauthService.decreaseExpirationBySec: ', this.oauthService.decreaseExpirationBySec);
this._oauthLogger.warn('issuedAtMSec - clockSkewInMSec >= now: ', issuedAtMSec - clockSkewInMSec >= now);
this._oauthLogger.warn(
'expiresAtMSec + clockSkewInMSec - this.oauthService.decreaseExpirationBySec <= now: ',
expiresAtMSec + clockSkewInMSec - this.oauthService.decreaseExpirationBySec <= now
);
}
}
}

View File

@@ -33,8 +33,6 @@ import { OAuthService, OAuthStorage } from 'angular-oauth2-oidc';
*
* See the related issue: https://github.com/manfredsteyer/angular-oauth2-oidc/issues/1443
*
* @implements {HttpInterceptor}
* @class
* @function intercept
* @param {HttpRequest<unknown>} request - The outgoing HTTP request.
* @param {HttpHandler} next - The next handler in the HTTP request chain.

View File

@@ -44,7 +44,6 @@ export class LocationCellComponent extends DataTableCellComponent implements OnI
super.ngOnInit();
}
/** @override */
protected updateValue(): void {
if (this.column?.key && this.column?.format && this.row && this.data) {
const path: PathInfo = this.data.getValue(this.row, this.column, this.resolverFn);

View File

@@ -146,8 +146,6 @@ export class FormModel implements ProcessFormModel {
/**
* Validates entire form and all form fields.
*
* @memberof FormModel
*/
validateForm(): void {
const validateFormEvent: any = new ValidateFormEvent(this);
@@ -173,7 +171,6 @@ export class FormModel implements ProcessFormModel {
* Validates a specific form field, triggers form validation.
*
* @param field Form field to validate.
* @memberof FormModel
*/
validateField(field: FormFieldModel): void {
if (!field) {

View File

@@ -20,22 +20,18 @@ import { CookieService } from '../common/services/cookie.service';
@Injectable()
export class CookieServiceMock extends CookieService {
/** @override */
isEnabled(): boolean {
return true;
}
/** @override */
getItem(key: string): string | null {
return this[key]?.data || null;
}
/** @override */
setItem(key: string, data: string, expiration: Date | null, path: string | null): void {
this[key] = { data, expiration, path };
}
/** @override */
clear() {
Object.keys(this).forEach((key) => {
if (Object.prototype.hasOwnProperty.call(this, key) && typeof this[key] !== 'function') {