oidc-client-rx/src/auth-state/auth-state.service.ts

330 lines
8.5 KiB
TypeScript

import { Injectable, inject } from '@outposts/injection-js';
import { BehaviorSubject, type Observable, throwError } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import type { OpenIdConfiguration } from '../config/openid-configuration';
import type { 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 type { AuthenticatedResult } from './auth-result';
import type { 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<AuthenticatedResult>(DEFAULT_AUTHRESULT);
get authenticated$(): Observable<AuthenticatedResult> {
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<AuthStateResult>(
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<boolean>(
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<boolean>(
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);
}
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 };
}
}