angular-auth-oidc-client
Version:
Angular Lib for OpenID Connect & OAuth2
1,110 lines (1,091 loc) • 283 kB
JavaScript
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