import { Injectable, inject } from 'injection-js'; import { BehaviorSubject, Observable, throwError } from 'rxjs'; import { distinctUntilChanged } from 'rxjs/operators'; import { OpenIdConfiguration } from '../config/openid-configuration'; import { AuthResult } from '../flows/callback-context'; 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 { TokenValidationService } from '../validation/token-validation.service'; import { AuthenticatedResult } from './auth-result'; import { AuthStateResult } from './auth-state'; const DEFAULT_AUTHRESULT = { isAuthenticated: false, allConfigsAuthenticated: [], }; @Injectable() export class AuthStateService { private readonly storagePersistenceService = inject( StoragePersistenceService ); private readonly loggerService = inject(LoggerService); private readonly publicEventsService = inject(PublicEventsService); private readonly tokenValidationService = inject(TokenValidationService); private readonly authenticatedInternal$ = new BehaviorSubject(DEFAULT_AUTHRESULT); get authenticated$(): Observable { return this.authenticatedInternal$ .asObservable() .pipe(distinctUntilChanged()); } setAuthenticatedAndFireEvent(allConfigs: OpenIdConfiguration[]): void { const result = this.composeAuthenticatedResult(allConfigs); this.authenticatedInternal$.next(result); } setUnauthenticatedAndFireEvent( currentConfig: OpenIdConfiguration, allConfigs: OpenIdConfiguration[] ): void { this.storagePersistenceService.resetAuthStateInStorage(currentConfig); const result = this.composeUnAuthenticatedResult(allConfigs); this.authenticatedInternal$.next(result); } updateAndPublishAuthState(authenticationResult: AuthStateResult): void { this.publicEventsService.fireEvent( EventTypes.NewAuthenticationResult, authenticationResult ); } setAuthorizationData( accessToken: string, authResult: AuthResult | null, currentConfig: OpenIdConfiguration, allConfigs: OpenIdConfiguration[] ): void { this.loggerService.logDebug( currentConfig, `storing the accessToken '${accessToken}'` ); this.storagePersistenceService.write( 'authzData', accessToken, currentConfig ); this.persistAccessTokenExpirationTime(authResult, currentConfig); this.setAuthenticatedAndFireEvent(allConfigs); } getAccessToken(configuration: OpenIdConfiguration | null): string { if (!configuration) { return ''; } if (!this.isAuthenticated(configuration)) { return ''; } const token = this.storagePersistenceService.getAccessToken(configuration); return this.decodeURIComponentSafely(token); } getIdToken(configuration: OpenIdConfiguration | null): string { if (!configuration) { return ''; } if (!this.isAuthenticated(configuration)) { return ''; } const token = this.storagePersistenceService.getIdToken(configuration); return this.decodeURIComponentSafely(token); } getRefreshToken(configuration: OpenIdConfiguration | null): string { if (!configuration) { return ''; } if (!this.isAuthenticated(configuration)) { return ''; } const token = this.storagePersistenceService.getRefreshToken(configuration); return this.decodeURIComponentSafely(token); } getAuthenticationResult( configuration: OpenIdConfiguration | null ): AuthResult | null { if (!configuration) { return null; } if (!this.isAuthenticated(configuration)) { return null; } return this.storagePersistenceService.getAuthenticationResult( configuration ); } areAuthStorageTokensValid( configuration: OpenIdConfiguration | null ): boolean { if (!configuration) { return false; } if (!this.isAuthenticated(configuration)) { return false; } if (this.hasIdTokenExpiredAndRenewCheckIsEnabled(configuration)) { this.loggerService.logDebug( configuration, 'persisted idToken is expired' ); return false; } if (this.hasAccessTokenExpiredIfExpiryExists(configuration)) { this.loggerService.logDebug( configuration, 'persisted accessToken is expired' ); return false; } this.loggerService.logDebug( configuration, 'persisted idToken and accessToken are valid' ); return true; } hasIdTokenExpiredAndRenewCheckIsEnabled( configuration: OpenIdConfiguration ): boolean { const { renewTimeBeforeTokenExpiresInSeconds, triggerRefreshWhenIdTokenExpired, disableIdTokenValidation, } = configuration; if (!triggerRefreshWhenIdTokenExpired || disableIdTokenValidation) { return false; } const tokenToCheck = this.storagePersistenceService.getIdToken(configuration); const idTokenExpired = this.tokenValidationService.hasIdTokenExpired( tokenToCheck, configuration, renewTimeBeforeTokenExpiresInSeconds ); if (idTokenExpired) { this.publicEventsService.fireEvent( EventTypes.IdTokenExpired, idTokenExpired ); } return idTokenExpired; } hasAccessTokenExpiredIfExpiryExists( configuration: OpenIdConfiguration ): boolean { const { renewTimeBeforeTokenExpiresInSeconds } = configuration; const accessTokenExpiresIn = this.storagePersistenceService.read( 'access_token_expires_at', configuration ); const accessTokenHasNotExpired = this.tokenValidationService.validateAccessTokenNotExpired( accessTokenExpiresIn, configuration, renewTimeBeforeTokenExpiresInSeconds ); const hasExpired = !accessTokenHasNotExpired; if (hasExpired) { this.publicEventsService.fireEvent( EventTypes.TokenExpired, hasExpired ); } return hasExpired; } isAuthenticated(configuration: OpenIdConfiguration | null): boolean { if (!configuration) { throwError( () => new Error( 'Please provide a configuration before setting up the module' ) ); return false; } const hasAccessToken = !!this.storagePersistenceService.getAccessToken(configuration); const hasIdToken = !!this.storagePersistenceService.getIdToken(configuration); return hasAccessToken && hasIdToken; } private decodeURIComponentSafely(token: string): string { if (token) { return decodeURIComponent(token); } else { return ''; } } private persistAccessTokenExpirationTime( authResult: AuthResult | null, configuration: OpenIdConfiguration ): void { if (authResult?.expires_in) { const accessTokenExpiryTime = new Date(new Date().toUTCString()).valueOf() + authResult.expires_in * 1000; this.storagePersistenceService.write( 'access_token_expires_at', accessTokenExpiryTime, configuration ); } } private composeAuthenticatedResult( allConfigs: OpenIdConfiguration[] ): AuthenticatedResult { if (allConfigs.length === 1) { const { configId } = allConfigs[0]; return { isAuthenticated: true, allConfigsAuthenticated: [ { configId: configId ?? '', isAuthenticated: true }, ], }; } return this.checkAllConfigsIfTheyAreAuthenticated(allConfigs); } private composeUnAuthenticatedResult( allConfigs: OpenIdConfiguration[] ): AuthenticatedResult { if (allConfigs.length === 1) { const { configId } = allConfigs[0]; return { isAuthenticated: false, allConfigsAuthenticated: [ { configId: configId ?? '', isAuthenticated: false }, ], }; } return this.checkAllConfigsIfTheyAreAuthenticated(allConfigs); } private checkAllConfigsIfTheyAreAuthenticated( allConfigs: OpenIdConfiguration[] ): AuthenticatedResult { const allConfigsAuthenticated = allConfigs.map((config) => ({ configId: config.configId ?? '', isAuthenticated: this.isAuthenticated(config), })); const isAuthenticated = allConfigsAuthenticated.every( (x) => !!x.isAuthenticated ); return { allConfigsAuthenticated, isAuthenticated }; } }