UNPKG

angular-auth-oidc-client

Version:
1,110 lines (1,091 loc) 283 kB
import { DOCUMENT, isPlatformBrowser, CommonModule } from '@angular/common'; import { HttpParams, HttpClient, HttpHeaders, HttpErrorResponse, HttpResponse, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import * as i0 from '@angular/core'; import { InjectionToken, Injectable, inject, NgZone, PLATFORM_ID, isDevMode, RendererFactory2, makeEnvironmentProviders, APP_INITIALIZER, NgModule } from '@angular/core'; import { of, forkJoin, ReplaySubject, from, BehaviorSubject, throwError, timer, Observable, Subject, TimeoutError } from 'rxjs'; import { map, mergeMap, tap, distinctUntilChanged, switchMap, retryWhen, catchError, retry, concatMap, finalize, take, timeout } from 'rxjs/operators'; import { base64url } from 'rfc4648'; import { Router } from '@angular/router'; import { toSignal } from '@angular/core/rxjs-interop'; class OpenIdConfigLoader { } class StsConfigLoader { } class StsConfigStaticLoader { constructor(passedConfigs) { this.passedConfigs = passedConfigs; } loadConfigs() { if (Array.isArray(this.passedConfigs)) { return of(this.passedConfigs); } return of([this.passedConfigs]); } } class StsConfigHttpLoader { constructor(configs$) { this.configs$ = configs$; } loadConfigs() { if (Array.isArray(this.configs$)) { return forkJoin(this.configs$); } const singleConfigOrArray = this.configs$; return singleConfigOrArray.pipe(map((value) => { if (Array.isArray(value)) { return value; } return [value]; })); } } function createStaticLoader(passedConfig) { if (!passedConfig?.config) { throw new Error('No config provided!'); } return new StsConfigStaticLoader(passedConfig.config); } const PASSED_CONFIG = new InjectionToken('PASSED_CONFIG'); /** * Implement this class-interface to create a custom logger service. */ class AbstractLoggerService { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: AbstractLoggerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: AbstractLoggerService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: AbstractLoggerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class ConsoleLoggerService { logError(message, ...args) { console.error(message, ...args); } logWarning(message, ...args) { console.warn(message, ...args); } logDebug(message, ...args) { console.debug(message, ...args); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ConsoleLoggerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ConsoleLoggerService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ConsoleLoggerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); var LogLevel; (function (LogLevel) { LogLevel[LogLevel["None"] = 0] = "None"; LogLevel[LogLevel["Debug"] = 1] = "Debug"; LogLevel[LogLevel["Warn"] = 2] = "Warn"; LogLevel[LogLevel["Error"] = 3] = "Error"; })(LogLevel || (LogLevel = {})); class LoggerService { constructor() { this.abstractLoggerService = inject(AbstractLoggerService); } logError(configuration, message, ...args) { if (this.loggingIsTurnedOff(configuration)) { return; } const { configId } = configuration; const messageToLog = this.isObject(message) ? JSON.stringify(message) : message; if (!!args && !!args.length) { this.abstractLoggerService.logError(`[ERROR] ${configId} - ${messageToLog}`, ...args); } else { this.abstractLoggerService.logError(`[ERROR] ${configId} - ${messageToLog}`); } } logWarning(configuration, message, ...args) { if (!this.logLevelIsSet(configuration)) { return; } if (this.loggingIsTurnedOff(configuration)) { return; } if (!this.currentLogLevelIsEqualOrSmallerThan(configuration, LogLevel.Warn)) { return; } const { configId } = configuration; const messageToLog = this.isObject(message) ? JSON.stringify(message) : message; if (!!args && !!args.length) { this.abstractLoggerService.logWarning(`[WARN] ${configId} - ${messageToLog}`, ...args); } else { this.abstractLoggerService.logWarning(`[WARN] ${configId} - ${messageToLog}`); } } logDebug(configuration, message, ...args) { if (!configuration) { return; } if (!this.logLevelIsSet(configuration)) { return; } if (this.loggingIsTurnedOff(configuration)) { return; } if (!this.currentLogLevelIsEqualOrSmallerThan(configuration, LogLevel.Debug)) { return; } const { configId } = configuration; const messageToLog = this.isObject(message) ? JSON.stringify(message) : message; if (!!args && !!args.length) { this.abstractLoggerService.logDebug(`[DEBUG] ${configId} - ${messageToLog}`, ...args); } else { this.abstractLoggerService.logDebug(`[DEBUG] ${configId} - ${messageToLog}`); } } currentLogLevelIsEqualOrSmallerThan(configuration, logLevelToCompare) { const { logLevel } = configuration || {}; if (!logLevel) { return false; } return logLevel <= logLevelToCompare; } logLevelIsSet(configuration) { const { logLevel } = configuration || {}; if (logLevel === null) { return false; } return logLevel !== undefined; } loggingIsTurnedOff(configuration) { const { logLevel } = configuration || {}; return logLevel === LogLevel.None; } isObject(possibleObject) { return Object.prototype.toString.call(possibleObject) === '[object Object]'; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: LoggerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: LoggerService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: LoggerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); var EventTypes; (function (EventTypes) { /** * This only works in the AppModule Constructor */ EventTypes[EventTypes["ConfigLoaded"] = 0] = "ConfigLoaded"; EventTypes[EventTypes["CheckingAuth"] = 1] = "CheckingAuth"; EventTypes[EventTypes["CheckingAuthFinished"] = 2] = "CheckingAuthFinished"; EventTypes[EventTypes["CheckingAuthFinishedWithError"] = 3] = "CheckingAuthFinishedWithError"; EventTypes[EventTypes["ConfigLoadingFailed"] = 4] = "ConfigLoadingFailed"; EventTypes[EventTypes["CheckSessionReceived"] = 5] = "CheckSessionReceived"; EventTypes[EventTypes["UserDataChanged"] = 6] = "UserDataChanged"; EventTypes[EventTypes["NewAuthenticationResult"] = 7] = "NewAuthenticationResult"; EventTypes[EventTypes["TokenExpired"] = 8] = "TokenExpired"; EventTypes[EventTypes["IdTokenExpired"] = 9] = "IdTokenExpired"; EventTypes[EventTypes["SilentRenewStarted"] = 10] = "SilentRenewStarted"; EventTypes[EventTypes["SilentRenewFailed"] = 11] = "SilentRenewFailed"; })(EventTypes || (EventTypes = {})); class PublicEventsService { constructor() { this.notify = new ReplaySubject(1); } /** * Fires a new event. * * @param type The event type. * @param value The event value. */ fireEvent(type, value) { this.notify.next({ type, value }); } /** * Wires up the event notification observable. */ registerForEvents() { return this.notify.asObservable(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: PublicEventsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: PublicEventsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: PublicEventsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Implement this class-interface to create a custom storage. */ class AbstractSecurityStorage { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: AbstractSecurityStorage, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: AbstractSecurityStorage, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: AbstractSecurityStorage, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class BrowserStorageService { constructor() { this.loggerService = inject(LoggerService); this.abstractSecurityStorage = inject(AbstractSecurityStorage); } read(key, configuration) { const { configId } = configuration; if (!configId) { this.loggerService.logDebug(configuration, `Wanted to read '${key}' but configId was '${configId}'`); return null; } if (!this.hasStorage()) { this.loggerService.logDebug(configuration, `Wanted to read '${key}' but Storage was undefined`); return null; } const storedConfig = this.abstractSecurityStorage.read(configId); if (!storedConfig) { return null; } return JSON.parse(storedConfig); } write(value, configuration) { const { configId } = configuration; if (!configId) { this.loggerService.logDebug(configuration, `Wanted to write but configId was '${configId}'`); return false; } if (!this.hasStorage()) { this.loggerService.logDebug(configuration, `Wanted to write but Storage was falsy`); return false; } value = value || null; this.abstractSecurityStorage.write(configId, JSON.stringify(value)); return true; } remove(key, configuration) { if (!this.hasStorage()) { this.loggerService.logDebug(configuration, `Wanted to remove '${key}' but Storage was falsy`); return false; } // const storage = this.getStorage(configuration); // if (!storage) { // this.loggerService.logDebug(configuration, `Wanted to write '${key}' but Storage was falsy`); // return false; // } this.abstractSecurityStorage.remove(key); return true; } // TODO THIS STORAGE WANTS AN ID BUT CLEARS EVERYTHING clear(configuration) { if (!this.hasStorage()) { this.loggerService.logDebug(configuration, `Wanted to clear storage but Storage was falsy`); return false; } // const storage = this.getStorage(configuration); // if (!storage) { // this.loggerService.logDebug(configuration, `Wanted to clear storage but Storage was falsy`); // return false; // } this.abstractSecurityStorage.clear(); return true; } hasStorage() { return typeof Storage !== 'undefined'; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: BrowserStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: BrowserStorageService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: BrowserStorageService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class StoragePersistenceService { constructor() { this.browserStorageService = inject(BrowserStorageService); } read(key, config) { const storedConfig = this.browserStorageService.read(key, config) || {}; return storedConfig[key]; } write(key, value, config) { const storedConfig = this.browserStorageService.read(key, config) || {}; storedConfig[key] = value; return this.browserStorageService.write(storedConfig, config); } remove(key, config) { const storedConfig = this.browserStorageService.read(key, config) || {}; delete storedConfig[key]; this.browserStorageService.write(storedConfig, config); } clear(config) { this.browserStorageService.clear(config); } resetStorageFlowData(config) { this.remove('session_state', config); this.remove('storageSilentRenewRunning', config); this.remove('storageCodeFlowInProgress', config); this.remove('codeVerifier', config); this.remove('userData', config); this.remove('storageCustomParamsAuthRequest', config); this.remove('access_token_expires_at', config); this.remove('storageCustomParamsRefresh', config); this.remove('storageCustomParamsEndSession', config); this.remove('reusable_refresh_token', config); } resetAuthStateInStorage(config) { this.remove('authzData', config); this.remove('reusable_refresh_token', config); this.remove('authnResult', config); } getAccessToken(config) { return this.read('authzData', config); } getIdToken(config) { return this.read('authnResult', config)?.id_token; } getRefreshToken(config) { const refreshToken = this.read('authnResult', config)?.refresh_token; if (!refreshToken && config.allowUnsafeReuseRefreshToken) { return this.read('reusable_refresh_token', config); } return refreshToken; } getAuthenticationResult(config) { return this.read('authnResult', config); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: StoragePersistenceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: StoragePersistenceService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: StoragePersistenceService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class JwkExtractor { extractJwk(keys, spec, throwOnEmpty = true) { if (0 === keys.length) { throw JwkExtractorInvalidArgumentError; } const foundKeys = keys .filter((k) => (spec?.kid ? k['kid'] === spec.kid : true)) .filter((k) => (spec?.use ? k['use'] === spec.use : true)) .filter((k) => (spec?.kty ? k['kty'] === spec.kty : true)); if (foundKeys.length === 0 && throwOnEmpty) { throw JwkExtractorNoMatchingKeysError; } if (foundKeys.length > 1 && (null === spec || undefined === spec)) { throw JwkExtractorSeveralMatchingKeysError; } return foundKeys; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JwkExtractor, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JwkExtractor, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JwkExtractor, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); function buildErrorName(name) { return JwkExtractor.name + ': ' + name; } const JwkExtractorInvalidArgumentError = { name: buildErrorName('InvalidArgumentError'), message: 'Array of keys was empty. Unable to extract', }; const JwkExtractorNoMatchingKeysError = { name: buildErrorName('NoMatchingKeysError'), message: 'No key found matching the spec', }; const JwkExtractorSeveralMatchingKeysError = { name: buildErrorName('SeveralMatchingKeysError'), message: 'More than one key found. Please use spec to filter', }; const PARTS_OF_TOKEN = 3; class TokenHelperService { constructor() { this.loggerService = inject(LoggerService); this.document = inject(DOCUMENT); } getTokenExpirationDate(dataIdToken) { if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'exp')) { return new Date(new Date().toUTCString()); } const date = new Date(0); // The 0 here is the key, which sets the date to the epoch date.setUTCSeconds(dataIdToken.exp); return date; } getSigningInputFromToken(token, encoded, configuration) { if (!this.tokenIsValid(token, configuration)) { return ''; } const header = this.getHeaderFromToken(token, encoded, configuration); const payload = this.getPayloadFromToken(token, encoded, configuration); return [header, payload].join('.'); } getHeaderFromToken(token, encoded, configuration) { if (!this.tokenIsValid(token, configuration)) { return {}; } return this.getPartOfToken(token, 0, encoded); } getPayloadFromToken(token, encoded, configuration) { if (!configuration) { return {}; } if (!this.tokenIsValid(token, configuration)) { return {}; } return this.getPartOfToken(token, 1, encoded); } getSignatureFromToken(token, encoded, configuration) { if (!this.tokenIsValid(token, configuration)) { return {}; } return this.getPartOfToken(token, 2, encoded); } getPartOfToken(token, index, encoded) { const partOfToken = this.extractPartOfToken(token, index); if (encoded) { return partOfToken; } const result = this.urlBase64Decode(partOfToken); return JSON.parse(result); } urlBase64Decode(str) { let output = str.replace(/-/g, '+').replace(/_/g, '/'); switch (output.length % 4) { case 0: break; case 2: output += '=='; break; case 3: output += '='; break; default: throw Error('Illegal base64url string!'); } const decoded = typeof this.document.defaultView !== 'undefined' ? this.document.defaultView?.atob(output) : Buffer.from(output, 'base64').toString('binary'); if (!decoded) { return ''; } try { // Going backwards: from byte stream, to percent-encoding, to original string. return decodeURIComponent(decoded .split('') .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) .join('')); } catch (err) { return decoded; } } tokenIsValid(token, configuration) { if (!token) { this.loggerService.logError(configuration, `token '${token}' is not valid --> token falsy`); return false; } if (!token.includes('.')) { this.loggerService.logError(configuration, `token '${token}' is not valid --> no dots included`); return false; } const parts = token.split('.'); if (parts.length !== PARTS_OF_TOKEN) { this.loggerService.logError(configuration, `token '${token}' is not valid --> token has to have exactly ${PARTS_OF_TOKEN - 1} dots`); return false; } return true; } extractPartOfToken(token, index) { return token.split('.')[index]; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TokenHelperService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TokenHelperService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TokenHelperService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class CryptoService { constructor() { this.document = inject(DOCUMENT); } getCrypto() { // support for IE, (window.crypto || window.msCrypto) return (this.document.defaultView?.crypto || this.document.defaultView?.msCrypto); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: CryptoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: CryptoService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: CryptoService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class JwkWindowCryptoService { constructor() { this.cryptoService = inject(CryptoService); } importVerificationKey(key, algorithm) { return this.cryptoService .getCrypto() .subtle.importKey('jwk', key, algorithm, false, ['verify']); } verifyKey(verifyAlgorithm, cryptoKey, signature, signingInput) { return this.cryptoService .getCrypto() .subtle.verify(verifyAlgorithm, cryptoKey, signature, new TextEncoder().encode(signingInput)); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JwkWindowCryptoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JwkWindowCryptoService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JwkWindowCryptoService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class JwtWindowCryptoService { constructor() { this.cryptoService = inject(CryptoService); } generateCodeChallenge(codeVerifier) { return this.calcHash(codeVerifier).pipe(map((challengeRaw) => this.base64UrlEncode(challengeRaw))); } generateAtHash(accessToken, algorithm) { return this.calcHash(accessToken, algorithm).pipe(map((tokenHash) => { const substr = tokenHash.substr(0, tokenHash.length / 2); const tokenHashBase64 = btoa(substr); return tokenHashBase64 .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); })); } calcHash(valueToHash, algorithm = 'SHA-256') { const msgBuffer = new TextEncoder().encode(valueToHash); return from(this.cryptoService.getCrypto().subtle.digest(algorithm, msgBuffer)).pipe(map((hashBuffer) => { const buffer = hashBuffer; const hashArray = Array.from(new Uint8Array(buffer)); return this.toHashString(hashArray); })); } toHashString(byteArray) { let result = ''; for (const e of byteArray) { result += String.fromCharCode(e); } return result; } base64UrlEncode(str) { const base64 = btoa(str); return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JwtWindowCryptoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JwtWindowCryptoService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JwtWindowCryptoService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); function getVerifyAlg(alg) { switch (alg.charAt(0)) { case 'R': return { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256', }; case 'E': if (alg.includes('256')) { return { name: 'ECDSA', hash: 'SHA-256', }; } else if (alg.includes('384')) { return { name: 'ECDSA', hash: 'SHA-384', }; } else { return null; } default: return null; } } function alg2kty(alg) { switch (alg.charAt(0)) { case 'R': return 'RSA'; case 'E': return 'EC'; default: throw new Error('Cannot infer kty from alg: ' + alg); } } function getImportAlg(alg) { switch (alg.charAt(0)) { case 'R': if (alg.includes('256')) { return { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256', }; } else if (alg.includes('384')) { return { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-384', }; } else if (alg.includes('512')) { return { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-512', }; } else { return null; } case 'E': if (alg.includes('256')) { return { name: 'ECDSA', namedCurve: 'P-256', }; } else if (alg.includes('384')) { return { name: 'ECDSA', namedCurve: 'P-384', }; } else { return null; } default: return null; } } // http://openid.net/specs/openid-connect-implicit-1_0.html // id_token // id_token C1: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) // MUST exactly match the value of the iss (issuer) Claim. // // id_token C2: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified // by the iss (issuer) Claim as an audience.The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, // or if it contains additional audiences not trusted by the Client. // // id_token C3: If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present. // // id_token C4: If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id is the Claim Value. // // id_token C5: The Client MUST validate the signature of the ID Token according to JWS [JWS] using the algorithm specified in the // alg Header Parameter of the JOSE Header.The Client MUST use the keys provided by the Issuer. // // id_token C6: The alg value SHOULD be RS256. Validation of tokens using other signing algorithms is described in the OpenID Connect // Core 1.0 // [OpenID.Core] specification. // // id_token C7: The current time MUST be before the time represented by the exp Claim (possibly allowing for some small leeway to account // for clock skew). // // id_token C8: The iat Claim can be used to reject tokens that were issued too far away from the current time, // limiting the amount of time that nonces need to be stored to prevent attacks.The acceptable range is Client specific. // // id_token C9: The value of the nonce Claim MUST be checked to verify that it is the same value as the one that was sent // in the Authentication Request.The Client SHOULD check the nonce value for replay attacks.The precise method for detecting replay attacks // is Client specific. // // id_token C10: If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate. // The meaning and processing of acr Claim Values is out of scope for this document. // // id_token C11: When a max_age request is made, the Client SHOULD check the auth_time Claim value and request re- authentication // if it determines too much time has elapsed since the last End- User authentication. // Access Token Validation // access_token C1: Hash the octets of the ASCII representation of the access_token with the hash algorithm specified in JWA[JWA] // for the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, the hash algorithm used is SHA-256. // access_token C2: Take the left- most half of the hash and base64url- encode it. // access_token C3: The value of at_hash in the ID Token MUST match the value produced in the previous step if at_hash is present // in the ID Token. class TokenValidationService { constructor() { this.keyAlgorithms = [ 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'PS256', 'PS384', 'PS512', ]; this.tokenHelperService = inject(TokenHelperService); this.loggerService = inject(LoggerService); this.jwkExtractor = inject(JwkExtractor); this.jwkWindowCryptoService = inject(JwkWindowCryptoService); this.jwtWindowCryptoService = inject(JwtWindowCryptoService); } static { this.refreshTokenNoncePlaceholder = '--RefreshToken--'; } // id_token C7: The current time MUST be before the time represented by the exp Claim // (possibly allowing for some small leeway to account for clock skew). hasIdTokenExpired(token, configuration, offsetSeconds) { const decoded = this.tokenHelperService.getPayloadFromToken(token, false, configuration); return !this.validateIdTokenExpNotExpired(decoded, configuration, offsetSeconds); } // id_token C7: The current time MUST be before the time represented by the exp Claim // (possibly allowing for some small leeway to account for clock skew). validateIdTokenExpNotExpired(decodedIdToken, configuration, offsetSeconds) { const tokenExpirationDate = this.tokenHelperService.getTokenExpirationDate(decodedIdToken); offsetSeconds = offsetSeconds || 0; if (!tokenExpirationDate) { return false; } const tokenExpirationValue = tokenExpirationDate.valueOf(); const nowWithOffset = this.calculateNowWithOffset(offsetSeconds); const tokenNotExpired = tokenExpirationValue > nowWithOffset; this.loggerService.logDebug(configuration, `Has idToken expired: ${!tokenNotExpired} --> expires in ${this.millisToMinutesAndSeconds(tokenExpirationValue - nowWithOffset)} , ${new Date(tokenExpirationValue).toLocaleTimeString()} > ${new Date(nowWithOffset).toLocaleTimeString()}`); return tokenNotExpired; } validateAccessTokenNotExpired(accessTokenExpiresAt, configuration, offsetSeconds) { // value is optional, so if it does not exist, then it has not expired if (!accessTokenExpiresAt) { return true; } offsetSeconds = offsetSeconds || 0; const accessTokenExpirationValue = accessTokenExpiresAt.valueOf(); const nowWithOffset = this.calculateNowWithOffset(offsetSeconds); const tokenNotExpired = accessTokenExpirationValue > nowWithOffset; this.loggerService.logDebug(configuration, `Has accessToken expired: ${!tokenNotExpired} --> expires in ${this.millisToMinutesAndSeconds(accessTokenExpirationValue - nowWithOffset)} , ${new Date(accessTokenExpirationValue).toLocaleTimeString()} > ${new Date(nowWithOffset).toLocaleTimeString()}`); return tokenNotExpired; } // iss // REQUIRED. Issuer Identifier for the Issuer of the response.The iss value is a case-sensitive URL using the // https scheme that contains scheme, host, // and optionally, port number and path components and no query or fragment components. // // sub // REQUIRED. Subject Identifier.Locally unique and never reassigned identifier within the Issuer for the End- User, // which is intended to be consumed by the Client, e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. // It MUST NOT exceed 255 ASCII characters in length.The sub value is a case-sensitive string. // // aud // REQUIRED. Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an // audience value. // It MAY also contain identifiers for other audiences.In the general case, the aud value is an array of case-sensitive strings. // In the common special case when there is one audience, the aud value MAY be a single case-sensitive string. // // exp // REQUIRED. Expiration time on or after which the ID Token MUST NOT be accepted for processing. // The processing of this parameter requires that the current date/ time MUST be before the expiration date/ time listed in the value. // Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. // Its value is a JSON [RFC7159] number representing the number of seconds from 1970- 01 - 01T00: 00:00Z as measured in UTC until // the date/ time. // See RFC 3339 [RFC3339] for details regarding date/ times in general and UTC in particular. // // iat // REQUIRED. Time at which the JWT was issued. Its value is a JSON number representing the number of seconds from // 1970- 01 - 01T00: 00: 00Z as measured // in UTC until the date/ time. validateRequiredIdToken(dataIdToken, configuration) { let validated = true; if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'iss')) { validated = false; this.loggerService.logWarning(configuration, 'iss is missing, this is required in the id_token'); } if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'sub')) { validated = false; this.loggerService.logWarning(configuration, 'sub is missing, this is required in the id_token'); } if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'aud')) { validated = false; this.loggerService.logWarning(configuration, 'aud is missing, this is required in the id_token'); } if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'exp')) { validated = false; this.loggerService.logWarning(configuration, 'exp is missing, this is required in the id_token'); } if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'iat')) { validated = false; this.loggerService.logWarning(configuration, 'iat is missing, this is required in the id_token'); } return validated; } // id_token C8: The iat Claim can be used to reject tokens that were issued too far away from the current time, // limiting the amount of time that nonces need to be stored to prevent attacks.The acceptable range is Client specific. validateIdTokenIatMaxOffset(dataIdToken, maxOffsetAllowedInSeconds, disableIatOffsetValidation, configuration) { if (disableIatOffsetValidation) { return true; } if (!Object.prototype.hasOwnProperty.call(dataIdToken, 'iat')) { return false; } const dateTimeIatIdToken = new Date(0); // The 0 here is the key, which sets the date to the epoch dateTimeIatIdToken.setUTCSeconds(dataIdToken.iat); maxOffsetAllowedInSeconds = maxOffsetAllowedInSeconds || 0; const nowInUtc = new Date(new Date().toUTCString()); const diff = nowInUtc.valueOf() - dateTimeIatIdToken.valueOf(); const maxOffsetAllowedInMilliseconds = maxOffsetAllowedInSeconds * 1000; this.loggerService.logDebug(configuration, `validate id token iat max offset ${diff} < ${maxOffsetAllowedInMilliseconds}`); if (diff > 0) { return diff < maxOffsetAllowedInMilliseconds; } return -diff < maxOffsetAllowedInMilliseconds; } // id_token C9: The value of the nonce Claim MUST be checked to verify that it is the same value as the one // that was sent in the Authentication Request.The Client SHOULD check the nonce value for replay attacks. // The precise method for detecting replay attacks is Client specific. // However the nonce claim SHOULD not be present for the refresh_token grant type // https://bitbucket.org/openid/connect/issues/1025/ambiguity-with-how-nonce-is-handled-on // The current spec is ambiguous and KeyCloak does send it. validateIdTokenNonce(dataIdToken, localNonce, ignoreNonceAfterRefresh, configuration) { const isFromRefreshToken = (dataIdToken.nonce === undefined || ignoreNonceAfterRefresh) && localNonce === TokenValidationService.refreshTokenNoncePlaceholder; if (!isFromRefreshToken && dataIdToken.nonce !== localNonce) { this.loggerService.logDebug(configuration, 'Validate_id_token_nonce failed, dataIdToken.nonce: ' + dataIdToken.nonce + ' local_nonce:' + localNonce); return false; } return true; } // id_token C1: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) // MUST exactly match the value of the iss (issuer) Claim. validateIdTokenIss(dataIdToken, authWellKnownEndpointsIssuer, configuration) { if (dataIdToken.iss !== authWellKnownEndpointsIssuer) { this.loggerService.logDebug(configuration, 'Validate_id_token_iss failed, dataIdToken.iss: ' + dataIdToken.iss + ' authWellKnownEndpoints issuer:' + authWellKnownEndpointsIssuer); return false; } return true; } // id_token C2: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified // by the iss (issuer) Claim as an audience. // The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences // not trusted by the Client. validateIdTokenAud(dataIdToken, aud, configuration) { if (Array.isArray(dataIdToken.aud)) { const result = dataIdToken.aud.includes(aud); if (!result) { this.loggerService.logDebug(configuration, 'Validate_id_token_aud array failed, dataIdToken.aud: ' + dataIdToken.aud + ' client_id:' + aud); return false; } return true; } else if (dataIdToken.aud !== aud) { this.loggerService.logDebug(configuration, 'Validate_id_token_aud failed, dataIdToken.aud: ' + dataIdToken.aud + ' client_id:' + aud); return false; } return true; } validateIdTokenAzpExistsIfMoreThanOneAud(dataIdToken) { if (!dataIdToken) { return false; } return !(Array.isArray(dataIdToken.aud) && dataIdToken.aud.length > 1 && !dataIdToken.azp); } // If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id is the Claim Value. validateIdTokenAzpValid(dataIdToken, clientId) { if (!dataIdToken?.azp) { return true; } return dataIdToken.azp === clientId; } validateStateFromHashCallback(state, localState, configuration) { if (state !== localState) { this.loggerService.logDebug(configuration, 'ValidateStateFromHashCallback failed, state: ' + state + ' local_state:' + localState); return false; } return true; } // id_token C5: The Client MUST validate the signature of the ID Token according to JWS [JWS] using the algorithm specified in the alg // Header Parameter of the JOSE Header.The Client MUST use the keys provided by the Issuer. // id_token C6: The alg value SHOULD be RS256. Validation of tokens using other signing algorithms is described in the // OpenID Connect Core 1.0 [OpenID.Core] specification. validateSignatureIdToken(idToken, jwtkeys, configuration) { if (!idToken) { return of(true); } if (!jwtkeys || !jwtkeys.keys) { return of(false); } const headerData = this.tokenHelperService.getHeaderFromToken(idToken, false, configuration); if (Object.keys(headerData).length === 0 && headerData.constructor === Object) { this.loggerService.logWarning(configuration, 'id token has no header data'); return of(false); } const kid = headerData.kid; const alg = headerData.alg; const keys = jwtkeys.keys; let foundKeys; let key; if (!this.keyAlgorithms.includes(alg)) { this.loggerService.logWarning(configuration, 'alg not supported', alg); return of(false); } const kty = alg2kty(alg); const use = 'sig'; try { foundKeys = kid ? this.jwkExtractor.extractJwk(keys, { kid, kty, use }, false) : this.jwkExtractor.extractJwk(keys, { kty, use }, false); if (foundKeys.length === 0) { foundKeys = kid ? this.jwkExtractor.extractJwk(keys, { kid, kty }) : this.jwkExtractor.extractJwk(keys, { kty }); } key = foundKeys[0]; } catch (e) { this.loggerService.logError(configuration, e); return of(false); } const algorithm = getImportAlg(alg); const signingInput = this.tokenHelperService.getSigningInputFromToken(idToken, true, configuration); const rawSignature = this.tokenHelperService.getSignatureFromToken(idToken, true, configuration); return from(this.jwkWindowCryptoService.importVerificationKey(key, algorithm)).pipe(mergeMap((cryptoKey) => { const signature = base64url.parse(rawSignature, { loose: true, }); const verifyAlgorithm = getVerifyAlg(alg); return from(this.jwkWindowCryptoService.verifyKey(verifyAlgorithm, cryptoKey, signature, signingInput)); }), tap((isValid) => { if (!isValid) { this.loggerService.logWarning(configuration, 'incorrect Signature, validation failed for id_token'); } })); } // Accepts ID Token without 'kid' claim in JOSE header if only one JWK supplied in 'jwks_url' //// private validate_no_kid_in_header_only_one_allowed_in_jwtkeys(header_data: any, jwtkeys: any): boolean { //// this.oidcSecurityCommon.logDebug('amount of jwtkeys.keys: ' + jwtkeys.keys.length); //// if (!header_data.hasOwnProperty('kid')) { //// // no kid defined in Jose header //// if (jwtkeys.keys.length != 1) { //// this.oidcSecurityCommon.logDebug('jwtkeys.keys.length != 1 and no kid in header'); //// return false; //// } //// } //// return true; //// } // Access Token Validation // access_token C1: Hash the octets of the ASCII representation of the access_token with the hash algorithm specified in JWA[JWA] // for the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, the hash algorithm used is SHA-256. // access_token C2: Take the left- most half of the hash and base64url- encode it. // access_token C3: The value of at_hash in the ID Token MUST match the value produced in the previous step if at_hash // is present in the ID Token. validateIdTokenAtHash(accessToken, atHash, idTokenAlg, configuration) { this.loggerService.logDebug(configuration, 'at_hash from the server:' + atHash); // 'sha256' 'sha384' 'sha512' let sha = 'SHA-256'; if (idTokenAlg.includes('384')) { sha = 'SHA-384'; } else if (idTokenAlg.includes('512')) { sha = 'SHA-512'; } return this.jwtWindowCryptoService .generateAtHash('' + accessToken, sha) .pipe(mergeMap((hash) => { this.loggerService.logDebug(configuration, 'at_hash client validation not decoded:' + hash); if (hash === atHash) { return of(true); // isValid; } else { return this.jwtWindowCryptoService .generateAtHash('' + decodeURIComponent(accessToken), sha) .pipe(map((newHash) => { this.loggerService.logDebug(configuration, '-gen access--' + hash); return newHash === atHash; })); } })); } millisToMinutesAndSeconds(millis) { const minutes = Math.floor(millis / 60000); const seconds = ((millis % 60000) / 1000).toFixed(0); return minutes + ':' + (+seconds < 10 ? '0' : '') + seconds; } calculateNowWithOffset(offsetSeconds) { return new Date(new Date().toUTCString()).valueOf() + offsetSeconds * 1000; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TokenValidationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TokenValidationService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TokenValidationService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); const DEFAULT_AUTHRESULT = { isAuthenticated: false, allConfigsAuthenticated: [], }; class AuthStateService { constructor() { this.storagePersistenceService = inject(StoragePersistenceService); this.loggerService = inject(LoggerService); this.publicEventsService = inject(PublicEventsService); this.tokenValidationService = inject(TokenValidationService); this.authenticatedInternal$ = new BehaviorSubject(DEFAULT_AUTHRESULT); } get authenticated$() { return this.authenticatedInternal$ .asObservable() .pipe(distinctUntilChanged()); } setAuthenticatedAndFireEvent(allConfigs) { const result = this.composeAuthenticatedResult(allConfigs); this.authenticatedInternal$.next(result); } setUnauthenticatedAndFireEvent(currentConfig, allConfigs) { this.storagePersistenceService.resetAuthStateInStorage(currentConfig); const result = this.composeUnAuthenticatedResult(allConfigs); this.authenticatedInternal$.next(result); } updateAndPublishAuthState(authenticationResult) { this.publicEventsService.fireEvent(EventTypes.NewAuthenticationResult, authenticationResult); } setAuthorizationData(accessToken, authResult, currentConfig, allConfigs) { this.loggerService.logDebug(currentConfig, `storing the accessToken '${accessToken}'`); this.storagePersistenceService.write('authzData', accessToken, currentConfig); this.persistAccessTokenExpirationTime(authResult, currentConfig); this.setAuthenticatedAndFireEvent(allConfigs); } getAccessToken(configuration) { if (!configuration) { return ''; } if (!this.isAuthenticated(configuration)) { return ''; } const token = this.storagePersistenceService.getAccessToken(configuration); return this.decodeURIComponentSafely(token); } getIdToken(configuration) { if (!configuration) { return ''; } if (!this.isAuthenticated(configuration)) { return ''; } const token = this.storagePersistenceService.getIdToken(configuration); return this.decodeURICompon