UNPKG

angular-oauth2-oidc

Version:

Support for OAuth 2(.1) and OpenId Connect (OIDC) in Angular

1,399 lines (1,385 loc) 126 kB
import * as i0 from '@angular/core'; import { Injectable, Optional, Inject, makeEnvironmentProviders, NgModule, InjectionToken } from '@angular/core'; import { DOCUMENT, CommonModule } from '@angular/common'; import * as i1 from '@angular/common/http'; import { HttpHeaders, HttpParams, HTTP_INTERCEPTORS } from '@angular/common/http'; import { Subject, of, from, race, throwError, combineLatest, merge } from 'rxjs'; import { filter, tap, debounceTime, delay, switchMap, map, first, catchError, timeout, take, mergeMap } from 'rxjs/operators'; /** * A validation handler that isn't validating nothing. * Can be used to skip validation (at your own risk). */ class NullValidationHandler { validateSignature(validationParams) { return Promise.resolve(null); } validateAtHash(validationParams) { return Promise.resolve(true); } } class OAuthModuleConfig { } class OAuthResourceServerConfig { } class DateTimeProvider { } class SystemDateTimeProvider extends DateTimeProvider { now() { return Date.now(); } new() { return new Date(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: SystemDateTimeProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: SystemDateTimeProvider }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: SystemDateTimeProvider, decorators: [{ type: Injectable }] }); /** * Additional options that can be passed to tryLogin. */ class LoginOptions { constructor() { /** * Set this to true to disable the nonce * check which is used to avoid * replay attacks. * This flag should never be true in * production environments. */ this.disableNonceCheck = false; /** * Normally, you want to clear your hash fragment after * the lib read the token(s) so that they are not displayed * anymore in the url. If not, set this to true. For code flow * this controls removing query string values. */ this.preventClearHashAfterLogin = false; } } /** * Defines the logging interface the OAuthService uses * internally. Is compatible with the `console` object, * but you can provide your own implementation as well * through dependency injection. */ class OAuthLogger { } /** * Defines a simple storage that can be used for * storing the tokens at client side. * Is compatible to localStorage and sessionStorage, * but you can also create your own implementations. */ class OAuthStorage { } class MemoryStorage { constructor() { this.data = new Map(); } getItem(key) { return this.data.get(key); } removeItem(key) { this.data.delete(key); } setItem(key, data) { this.data.set(key, data); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: MemoryStorage, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: MemoryStorage }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: MemoryStorage, decorators: [{ type: Injectable }] }); /** * Represents the received tokens, the received state * and the parsed claims from the id-token. */ class ReceivedTokens { } class OAuthEvent { constructor(type) { this.type = type; } } class OAuthSuccessEvent extends OAuthEvent { constructor(type, info = null) { super(type); this.info = info; } } class OAuthInfoEvent extends OAuthEvent { constructor(type, info = null) { super(type); this.info = info; } } class OAuthErrorEvent extends OAuthEvent { constructor(type, reason, params = null) { super(type); this.reason = reason; this.params = params; } } // see: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_.22Unicode_Problem.22 function b64DecodeUnicode(str) { const base64 = str.replace(/-/g, '+').replace(/_/g, '/'); return decodeURIComponent(atob(base64) .split('') .map(function (c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }) .join('')); } function base64UrlEncode(str) { const base64 = btoa(str); return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } class AuthConfig { constructor(json) { /** * The client's id as registered with the auth server */ this.clientId = ''; /** * The client's redirectUri as registered with the auth server */ this.redirectUri = ''; /** * An optional second redirectUri where the auth server * redirects the user to after logging out. */ this.postLogoutRedirectUri = ''; /** * Defines whether to use 'redirectUri' as a replacement * of 'postLogoutRedirectUri' if the latter is not set. */ this.redirectUriAsPostLogoutRedirectUriFallback = true; /** * The auth server's endpoint that allows to log * the user in when using implicit flow. */ this.loginUrl = ''; /** * The requested scopes */ this.scope = 'openid profile'; this.resource = ''; this.rngUrl = ''; /** * Defines whether to use OpenId Connect during * implicit flow. */ this.oidc = true; /** * Defines whether to request an access token during * implicit flow. */ this.requestAccessToken = true; this.options = null; /** * The issuer's uri. */ this.issuer = ''; /** * The logout url. */ this.logoutUrl = ''; /** * Defines whether to clear the hash fragment after logging in. */ this.clearHashAfterLogin = true; /** * Url of the token endpoint as defined by OpenId Connect and OAuth 2. */ this.tokenEndpoint = null; /** * Url of the revocation endpoint as defined by OpenId Connect and OAuth 2. */ this.revocationEndpoint = null; /** * Names of known parameters sent out in the TokenResponse. https://tools.ietf.org/html/rfc6749#section-5.1 */ this.customTokenParameters = []; /** * Url of the userinfo endpoint as defined by OpenId Connect. */ this.userinfoEndpoint = null; this.responseType = ''; /** * Defines whether additional debug information should * be shown at the console. Note that in certain browsers * the verbosity of the console needs to be explicitly set * to include Debug level messages. */ this.showDebugInformation = false; /** * The redirect uri used when doing silent refresh. */ this.silentRefreshRedirectUri = ''; this.silentRefreshMessagePrefix = ''; /** * Set this to true to display the iframe used for * silent refresh for debugging. */ this.silentRefreshShowIFrame = false; /** * Timeout for silent refresh. * @internal * @deprecated use silentRefreshTimeout */ this.siletRefreshTimeout = 1000 * 20; /** * Timeout for silent refresh. */ this.silentRefreshTimeout = 1000 * 20; /** * Some auth servers don't allow using password flow * w/o a client secret while the standards do not * demand for it. In this case, you can set a password * here. As this password is exposed to the public * it does not bring additional security and is therefore * as good as using no password. */ this.dummyClientSecret = ''; /** * Defines whether https is required. * The default value is remoteOnly which only allows * http for localhost, while every other domains need * to be used with https. */ this.requireHttps = 'remoteOnly'; /** * Defines whether every url provided by the discovery * document has to start with the issuer's url. */ this.strictDiscoveryDocumentValidation = true; /** * JSON Web Key Set (https://tools.ietf.org/html/rfc7517) * with keys used to validate received id_tokens. * This is taken out of the disovery document. Can be set manually too. */ this.jwks = null; /** * Map with additional query parameter that are appended to * the request when initializing implicit flow. */ this.customQueryParams = null; this.silentRefreshIFrameName = 'angular-oauth-oidc-silent-refresh-iframe'; /** * Defines when the token_timeout event should be raised. * If you set this to the default value 0.75, the event * is triggered after 75% of the token's life time. */ this.timeoutFactor = 0.75; /** * If true, the lib will try to check whether the user * is still logged in on a regular basis as described * in http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification */ this.sessionChecksEnabled = false; /** * Interval in msec for checking the session * according to http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification */ this.sessionCheckIntervall = 3 * 1000; /** * Url for the iframe used for session checks */ this.sessionCheckIFrameUrl = null; /** * Name of the iframe to use for session checks */ this.sessionCheckIFrameName = 'angular-oauth-oidc-check-session-iframe'; /** * This property has been introduced to disable at_hash checks * and is indented for Identity Provider that does not deliver * an at_hash EVEN THOUGH its recommended by the OIDC specs. * Of course, when disabling these checks then we are bypassing * a security check which means we are more vulnerable. */ this.disableAtHashCheck = false; /** * Defines wether to check the subject of a refreshed token after silent refresh. * Normally, it should be the same as before. */ this.skipSubjectCheck = false; this.useIdTokenHintForSilentRefresh = false; /** * Defined whether to skip the validation of the issuer in the discovery document. * Normally, the discovey document's url starts with the url of the issuer. */ this.skipIssuerCheck = false; /** * final state sent to issuer is built as follows: * state = nonce + nonceStateSeparator + additional state * Default separator is ';' (encoded %3B). * In rare cases, this character might be forbidden or inconvenient to use by the issuer so it can be customized. */ this.nonceStateSeparator = ';'; /** * Set this to true to use HTTP BASIC auth for AJAX calls */ this.useHttpBasicAuth = false; /** * Decreases the Expiration time of tokens by this number of seconds */ this.decreaseExpirationBySec = 0; /** * The interceptors waits this time span if there is no token */ this.waitForTokenInMsec = 0; /** * Code Flow is by defauld used together with PKCI which is also higly recommented. * You can disbale it here by setting this flag to true. * https://tools.ietf.org/html/rfc7636#section-1.1 */ this.disablePKCE = false; /** * Set this to true to preserve the requested route including query parameters after code flow login. * This setting enables deep linking for the code flow. */ this.preserveRequestedRoute = false; /** * Allows to disable the timer for the id_token used * for token refresh */ this.disableIdTokenTimer = false; /** * Blocks other origins requesting a silent refresh */ this.checkOrigin = false; /** * This property allows you to override the method that is used to open the login url, * allowing a way for implementations to specify their own method of routing to new * urls. */ this.openUri = (uri) => { location.href = uri; }; if (json) { Object.assign(this, json); } } } /** * This custom encoder allows charactes like +, % and / to be used in passwords */ class WebHttpUrlEncodingCodec { encodeKey(k) { return encodeURIComponent(k); } encodeValue(v) { return encodeURIComponent(v); } decodeKey(k) { return decodeURIComponent(k); } decodeValue(v) { return decodeURIComponent(v); } } /** * Interface for Handlers that are hooked in to * validate tokens. */ class ValidationHandler { } /** * This abstract implementation of ValidationHandler already implements * the method validateAtHash. However, to make use of it, * you have to override the method calcHash. */ class AbstractValidationHandler { /** * Validates the at_hash in an id_token against the received access_token. */ async validateAtHash(params) { const hashAlg = this.inferHashAlgorithm(params.idTokenHeader); const tokenHash = await this.calcHash(params.accessToken, hashAlg); // sha256(accessToken, { asString: true }); const leftMostHalf = tokenHash.substr(0, tokenHash.length / 2); const atHash = base64UrlEncode(leftMostHalf); const claimsAtHash = params.idTokenClaims['at_hash'].replace(/=/g, ''); if (atHash !== claimsAtHash) { console.error('exptected at_hash: ' + atHash); console.error('actual at_hash: ' + claimsAtHash); } return atHash === claimsAtHash; } /** * Infers the name of the hash algorithm to use * from the alg field of an id_token. * * @param jwtHeader the id_token's parsed header */ inferHashAlgorithm(jwtHeader) { const alg = jwtHeader['alg']; if (!alg.match(/^.S[0-9]{3}$/)) { throw new Error('Algorithm not supported: ' + alg); } return 'sha-' + alg.substr(2); } } class UrlHelperService { getHashFragmentParams(customHashFragment) { let hash = customHashFragment || window.location.hash; hash = decodeURIComponent(hash); if (hash.indexOf('#') !== 0) { return {}; } const questionMarkPosition = hash.indexOf('?'); if (questionMarkPosition > -1) { hash = hash.substr(questionMarkPosition + 1); } else { hash = hash.substr(1); } return this.parseQueryString(hash); } parseQueryString(queryString) { const data = {}; let pair, separatorIndex, escapedKey, escapedValue, key, value; if (queryString === null) { return data; } const pairs = queryString.split('&'); for (let i = 0; i < pairs.length; i++) { pair = pairs[i]; separatorIndex = pair.indexOf('='); if (separatorIndex === -1) { escapedKey = pair; escapedValue = null; } else { escapedKey = pair.substr(0, separatorIndex); escapedValue = pair.substr(separatorIndex + 1); } key = decodeURIComponent(escapedKey); value = decodeURIComponent(escapedValue); if (key.substr(0, 1) === '/') { key = key.substr(1); } data[key] = value; } return data; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: UrlHelperService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: UrlHelperService }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: UrlHelperService, decorators: [{ type: Injectable }] }); // Credits: https://github.com/dchest/fast-sha256-js/tree/master/src // We add this lib directly b/c the published version of fast-sha256-js // is commonjs and hence leads to a warning about tree-shakability emitted // by the Angular CLI // SHA-256 (+ HMAC and PBKDF2) for JavaScript. // // Written in 2014-2016 by Dmitry Chestnykh. // Public domain, no warranty. // // Functions (accept and return Uint8Arrays): // // sha256(message) -> hash // sha256.hmac(key, message) -> mac // sha256.pbkdf2(password, salt, rounds, dkLen) -> dk // // Classes: // // new sha256.Hash() // new sha256.HMAC(key) // const digestLength = 32; const blockSize = 64; // SHA-256 constants const K = new Uint32Array([ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, ]); function hashBlocks(w, v, p, pos, len) { let a, b, c, d, e, f, g, h, u, i, j, t1, t2; while (len >= 64) { a = v[0]; b = v[1]; c = v[2]; d = v[3]; e = v[4]; f = v[5]; g = v[6]; h = v[7]; for (i = 0; i < 16; i++) { j = pos + i * 4; w[i] = ((p[j] & 0xff) << 24) | ((p[j + 1] & 0xff) << 16) | ((p[j + 2] & 0xff) << 8) | (p[j + 3] & 0xff); } for (i = 16; i < 64; i++) { u = w[i - 2]; t1 = ((u >>> 17) | (u << (32 - 17))) ^ ((u >>> 19) | (u << (32 - 19))) ^ (u >>> 10); u = w[i - 15]; t2 = ((u >>> 7) | (u << (32 - 7))) ^ ((u >>> 18) | (u << (32 - 18))) ^ (u >>> 3); w[i] = ((t1 + w[i - 7]) | 0) + ((t2 + w[i - 16]) | 0); } for (i = 0; i < 64; i++) { t1 = ((((((e >>> 6) | (e << (32 - 6))) ^ ((e >>> 11) | (e << (32 - 11))) ^ ((e >>> 25) | (e << (32 - 25)))) + ((e & f) ^ (~e & g))) | 0) + ((h + ((K[i] + w[i]) | 0)) | 0)) | 0; t2 = ((((a >>> 2) | (a << (32 - 2))) ^ ((a >>> 13) | (a << (32 - 13))) ^ ((a >>> 22) | (a << (32 - 22)))) + ((a & b) ^ (a & c) ^ (b & c))) | 0; h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0; } v[0] += a; v[1] += b; v[2] += c; v[3] += d; v[4] += e; v[5] += f; v[6] += g; v[7] += h; pos += 64; len -= 64; } return pos; } // Hash implements SHA256 hash algorithm. class Hash { constructor() { this.digestLength = digestLength; this.blockSize = blockSize; // Note: Int32Array is used instead of Uint32Array for performance reasons. this.state = new Int32Array(8); // hash state this.temp = new Int32Array(64); // temporary state this.buffer = new Uint8Array(128); // buffer for data to hash this.bufferLength = 0; // number of bytes in buffer this.bytesHashed = 0; // number of total bytes hashed this.finished = false; // indicates whether the hash was finalized this.reset(); } // Resets hash state making it possible // to re-use this instance to hash other data. reset() { this.state[0] = 0x6a09e667; this.state[1] = 0xbb67ae85; this.state[2] = 0x3c6ef372; this.state[3] = 0xa54ff53a; this.state[4] = 0x510e527f; this.state[5] = 0x9b05688c; this.state[6] = 0x1f83d9ab; this.state[7] = 0x5be0cd19; this.bufferLength = 0; this.bytesHashed = 0; this.finished = false; return this; } // Cleans internal buffers and re-initializes hash state. clean() { for (let i = 0; i < this.buffer.length; i++) { this.buffer[i] = 0; } for (let i = 0; i < this.temp.length; i++) { this.temp[i] = 0; } this.reset(); } // Updates hash state with the given data. // // Optionally, length of the data can be specified to hash // fewer bytes than data.length. // // Throws error when trying to update already finalized hash: // instance must be reset to use it again. update(data, dataLength = data.length) { if (this.finished) { throw new Error("SHA256: can't update because hash was finished."); } let dataPos = 0; this.bytesHashed += dataLength; if (this.bufferLength > 0) { while (this.bufferLength < 64 && dataLength > 0) { this.buffer[this.bufferLength++] = data[dataPos++]; dataLength--; } if (this.bufferLength === 64) { hashBlocks(this.temp, this.state, this.buffer, 0, 64); this.bufferLength = 0; } } if (dataLength >= 64) { dataPos = hashBlocks(this.temp, this.state, data, dataPos, dataLength); dataLength %= 64; } while (dataLength > 0) { this.buffer[this.bufferLength++] = data[dataPos++]; dataLength--; } return this; } // Finalizes hash state and puts hash into out. // // If hash was already finalized, puts the same value. finish(out) { if (!this.finished) { const bytesHashed = this.bytesHashed; const left = this.bufferLength; const bitLenHi = (bytesHashed / 0x20000000) | 0; const bitLenLo = bytesHashed << 3; const padLength = bytesHashed % 64 < 56 ? 64 : 128; this.buffer[left] = 0x80; for (let i = left + 1; i < padLength - 8; i++) { this.buffer[i] = 0; } this.buffer[padLength - 8] = (bitLenHi >>> 24) & 0xff; this.buffer[padLength - 7] = (bitLenHi >>> 16) & 0xff; this.buffer[padLength - 6] = (bitLenHi >>> 8) & 0xff; this.buffer[padLength - 5] = (bitLenHi >>> 0) & 0xff; this.buffer[padLength - 4] = (bitLenLo >>> 24) & 0xff; this.buffer[padLength - 3] = (bitLenLo >>> 16) & 0xff; this.buffer[padLength - 2] = (bitLenLo >>> 8) & 0xff; this.buffer[padLength - 1] = (bitLenLo >>> 0) & 0xff; hashBlocks(this.temp, this.state, this.buffer, 0, padLength); this.finished = true; } for (let i = 0; i < 8; i++) { out[i * 4 + 0] = (this.state[i] >>> 24) & 0xff; out[i * 4 + 1] = (this.state[i] >>> 16) & 0xff; out[i * 4 + 2] = (this.state[i] >>> 8) & 0xff; out[i * 4 + 3] = (this.state[i] >>> 0) & 0xff; } return this; } // Returns the final hash digest. digest() { const out = new Uint8Array(this.digestLength); this.finish(out); return out; } // Internal function for use in HMAC for optimization. _saveState(out) { for (let i = 0; i < this.state.length; i++) { out[i] = this.state[i]; } } // Internal function for use in HMAC for optimization. _restoreState(from, bytesHashed) { for (let i = 0; i < this.state.length; i++) { this.state[i] = from[i]; } this.bytesHashed = bytesHashed; this.finished = false; this.bufferLength = 0; } } // HMAC implements HMAC-SHA256 message authentication algorithm. class HMAC { constructor(key) { this.inner = new Hash(); this.outer = new Hash(); this.blockSize = this.inner.blockSize; this.digestLength = this.inner.digestLength; const pad = new Uint8Array(this.blockSize); if (key.length > this.blockSize) { new Hash().update(key).finish(pad).clean(); } else { for (let i = 0; i < key.length; i++) { pad[i] = key[i]; } } for (let i = 0; i < pad.length; i++) { pad[i] ^= 0x36; } this.inner.update(pad); for (let i = 0; i < pad.length; i++) { pad[i] ^= 0x36 ^ 0x5c; } this.outer.update(pad); this.istate = new Uint32Array(8); this.ostate = new Uint32Array(8); this.inner._saveState(this.istate); this.outer._saveState(this.ostate); for (let i = 0; i < pad.length; i++) { pad[i] = 0; } } // Returns HMAC state to the state initialized with key // to make it possible to run HMAC over the other data with the same // key without creating a new instance. reset() { this.inner._restoreState(this.istate, this.inner.blockSize); this.outer._restoreState(this.ostate, this.outer.blockSize); return this; } // Cleans HMAC state. clean() { for (let i = 0; i < this.istate.length; i++) { this.ostate[i] = this.istate[i] = 0; } this.inner.clean(); this.outer.clean(); } // Updates state with provided data. update(data) { this.inner.update(data); return this; } // Finalizes HMAC and puts the result in out. finish(out) { if (this.outer.finished) { this.outer.finish(out); } else { this.inner.finish(out); this.outer.update(out, this.digestLength).finish(out); } return this; } // Returns message authentication code. digest() { const out = new Uint8Array(this.digestLength); this.finish(out); return out; } } // Returns SHA256 hash of data. function hash(data) { const h = new Hash().update(data); const digest = h.digest(); h.clean(); return digest; } // Returns HMAC-SHA256 of data under the key. function hmac(key, data) { const h = new HMAC(key).update(data); const digest = h.digest(); h.clean(); return digest; } // Fills hkdf buffer like this: // T(1) = HMAC-Hash(PRK, T(0) | info | 0x01) function fillBuffer(buffer, hmac, info, counter) { // Counter is a byte value: check if it overflowed. const num = counter[0]; if (num === 0) { throw new Error('hkdf: cannot expand more'); } // Prepare HMAC instance for new data with old key. hmac.reset(); // Hash in previous output if it was generated // (i.e. counter is greater than 1). if (num > 1) { hmac.update(buffer); } // Hash in info if it exists. if (info) { hmac.update(info); } // Hash in the counter. hmac.update(counter); // Output result to buffer and clean HMAC instance. hmac.finish(buffer); // Increment counter inside typed array, this works properly. counter[0]++; } const hkdfSalt = new Uint8Array(digestLength); // Filled with zeroes. function hkdf(key, salt = hkdfSalt, info, length = 32) { const counter = new Uint8Array([1]); // HKDF-Extract uses salt as HMAC key, and key as data. const okm = hmac(salt, key); // Initialize HMAC for expanding with extracted key. // Ensure no collisions with `hmac` function. const hmac_ = new HMAC(okm); // Allocate buffer. const buffer = new Uint8Array(hmac_.digestLength); let bufpos = buffer.length; const out = new Uint8Array(length); for (let i = 0; i < length; i++) { if (bufpos === buffer.length) { fillBuffer(buffer, hmac_, info, counter); bufpos = 0; } out[i] = buffer[bufpos++]; } hmac_.clean(); buffer.fill(0); counter.fill(0); return out; } // Derives a key from password and salt using PBKDF2-HMAC-SHA256 // with the given number of iterations. // // The number of bytes returned is equal to dkLen. // // (For better security, avoid dkLen greater than hash length - 32 bytes). function pbkdf2(password, salt, iterations, dkLen) { const prf = new HMAC(password); const len = prf.digestLength; const ctr = new Uint8Array(4); const t = new Uint8Array(len); const u = new Uint8Array(len); const dk = new Uint8Array(dkLen); for (let i = 0; i * len < dkLen; i++) { const c = i + 1; ctr[0] = (c >>> 24) & 0xff; ctr[1] = (c >>> 16) & 0xff; ctr[2] = (c >>> 8) & 0xff; ctr[3] = (c >>> 0) & 0xff; prf.reset(); prf.update(salt); prf.update(ctr); prf.finish(u); for (let j = 0; j < len; j++) { t[j] = u[j]; } for (let j = 2; j <= iterations; j++) { prf.reset(); prf.update(u).finish(u); for (let k = 0; k < len; k++) { t[k] ^= u[k]; } } for (let j = 0; j < len && i * len + j < dkLen; j++) { dk[i * len + j] = t[j]; } } for (let i = 0; i < len; i++) { t[i] = u[i] = 0; } for (let i = 0; i < 4; i++) { ctr[i] = 0; } prf.clean(); return dk; } /** * Abstraction for crypto algorithms */ class HashHandler { } function decodeUTF8(s) { if (typeof s !== 'string') throw new TypeError('expected string'); const d = s, b = new Uint8Array(d.length); for (let i = 0; i < d.length; i++) b[i] = d.charCodeAt(i); return b; } function encodeUTF8(arr) { const s = []; for (let i = 0; i < arr.length; i++) s.push(String.fromCharCode(arr[i])); return s.join(''); } class DefaultHashHandler { async calcHash(valueToHash, algorithm) { // const encoder = new TextEncoder(); // const hashArray = await window.crypto.subtle.digest(algorithm, data); // const data = encoder.encode(valueToHash); // const fhash = fsha256(valueToHash); const candHash = encodeUTF8(hash(decodeUTF8(valueToHash))); // const hashArray = (sha256 as any).array(valueToHash); // // const hashString = this.toHashString(hashArray); // const hashString = this.toHashString2(hashArray); // console.debug('hash orig - cand', candHash, hashString); // alert(1); return candHash; } toHashString2(byteArray) { let result = ''; for (const e of byteArray) { result += String.fromCharCode(e); } return result; } toHashString(buffer) { const byteArray = new Uint8Array(buffer); let result = ''; for (const e of byteArray) { result += String.fromCharCode(e); } return result; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: DefaultHashHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: DefaultHashHandler }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: DefaultHashHandler, decorators: [{ type: Injectable }] }); /** * Service for logging in and logging out with * OIDC and OAuth2. Supports implicit flow and * password flow. */ class OAuthService extends AuthConfig { constructor(ngZone, http, storage, tokenValidationHandler, config, urlHelper, logger, crypto, document, dateTimeService) { super(); this.ngZone = ngZone; this.http = http; this.config = config; this.urlHelper = urlHelper; this.logger = logger; this.crypto = crypto; this.dateTimeService = dateTimeService; /** * @internal * Deprecated: use property events instead */ this.discoveryDocumentLoaded = false; /** * The received (passed around) state, when logging * in with implicit flow. */ this.state = ''; this.eventsSubject = new Subject(); this.discoveryDocumentLoadedSubject = new Subject(); this.grantTypesSupported = []; this.inImplicitFlow = false; this.saveNoncesInLocalStorage = false; this.debug('angular-oauth2-oidc v10'); // See https://github.com/manfredsteyer/angular-oauth2-oidc/issues/773 for why this is needed this.document = document; if (!config) { config = {}; } this.discoveryDocumentLoaded$ = this.discoveryDocumentLoadedSubject.asObservable(); this.events = this.eventsSubject.asObservable(); if (tokenValidationHandler) { this.tokenValidationHandler = tokenValidationHandler; } if (config) { this.configure(config); } try { if (storage) { this.setStorage(storage); } else if (typeof sessionStorage !== 'undefined') { this.setStorage(sessionStorage); } } catch (e) { console.error('No OAuthStorage provided and cannot access default (sessionStorage).' + 'Consider providing a custom OAuthStorage implementation in your module.', e); } // in IE, sessionStorage does not always survive a redirect if (this.checkLocalStorageAccessable()) { const ua = window?.navigator?.userAgent; const msie = ua?.includes('MSIE ') || ua?.includes('Trident'); if (msie) { this.saveNoncesInLocalStorage = true; } } this.setupRefreshTimer(); } checkLocalStorageAccessable() { if (typeof window === 'undefined') return false; const test = 'test'; try { if (typeof window['localStorage'] === 'undefined') return false; localStorage.setItem(test, test); localStorage.removeItem(test); return true; } catch (e) { return false; } } /** * Use this method to configure the service * @param config the configuration */ configure(config) { // For the sake of downward compatibility with // original configuration API Object.assign(this, new AuthConfig(), config); this.config = Object.assign({}, new AuthConfig(), config); if (this.sessionChecksEnabled) { this.setupSessionCheck(); } this.configChanged(); } configChanged() { this.setupRefreshTimer(); } restartSessionChecksIfStillLoggedIn() { if (this.hasValidIdToken()) { this.initSessionCheck(); } } restartRefreshTimerIfStillLoggedIn() { this.setupExpirationTimers(); } setupSessionCheck() { this.events .pipe(filter((e) => e.type === 'token_received')) .subscribe(() => { this.initSessionCheck(); }); } /** * Will setup up silent refreshing for when the token is * about to expire. When the user is logged out via this.logOut method, the * silent refreshing will pause and not refresh the tokens until the user is * logged back in via receiving a new token. * @param params Additional parameter to pass * @param listenTo Setup automatic refresh of a specific token type */ setupAutomaticSilentRefresh(params = {}, listenTo, noPrompt = true) { let shouldRunSilentRefresh = true; this.clearAutomaticRefreshTimer(); this.automaticRefreshSubscription = this.events .pipe(tap((e) => { if (e.type === 'token_received') { shouldRunSilentRefresh = true; } else if (e.type === 'logout') { shouldRunSilentRefresh = false; } }), filter((e) => e.type === 'token_expires' && (listenTo == null || listenTo === 'any' || e.info === listenTo)), debounceTime(1000)) .subscribe(() => { if (shouldRunSilentRefresh) { // this.silentRefresh(params, noPrompt).catch(_ => { this.refreshInternal(params, noPrompt).catch(() => { this.debug('Automatic silent refresh did not work'); }); } }); this.restartRefreshTimerIfStillLoggedIn(); } refreshInternal(params, noPrompt) { if (!this.useSilentRefresh && this.responseType === 'code') { return this.refreshToken(); } else { return this.silentRefresh(params, noPrompt); } } /** * Convenience method that first calls `loadDiscoveryDocument(...)` and * directly chains using the `then(...)` part of the promise to call * the `tryLogin(...)` method. * * @param options LoginOptions to pass through to `tryLogin(...)` */ loadDiscoveryDocumentAndTryLogin(options = null) { return this.loadDiscoveryDocument().then(() => { return this.tryLogin(options); }); } /** * Convenience method that first calls `loadDiscoveryDocumentAndTryLogin(...)` * and if then chains to `initLoginFlow()`, but only if there is no valid * IdToken or no valid AccessToken. * * @param options LoginOptions to pass through to `tryLogin(...)` */ loadDiscoveryDocumentAndLogin(options = null) { options = options || {}; return this.loadDiscoveryDocumentAndTryLogin(options).then(() => { if (!this.hasValidIdToken() || !this.hasValidAccessToken()) { const state = typeof options.state === 'string' ? options.state : ''; this.initLoginFlow(state); return false; } else { return true; } }); } debug(...args) { if (this.showDebugInformation) { this.logger.debug(...args); } } validateUrlFromDiscoveryDocument(url) { const errors = []; const httpsCheck = this.validateUrlForHttps(url); const issuerCheck = this.validateUrlAgainstIssuer(url); if (!httpsCheck) { errors.push('https for all urls required. Also for urls received by discovery.'); } if (!issuerCheck) { errors.push('Every url in discovery document has to start with the issuer url.' + 'Also see property strictDiscoveryDocumentValidation.'); } return errors; } validateUrlForHttps(url) { if (!url) { return true; } const lcUrl = url.toLowerCase(); if (this.requireHttps === false) { return true; } if ((lcUrl.match(/^http:\/\/localhost($|[:/])/) || lcUrl.match(/^http:\/\/localhost($|[:/])/)) && this.requireHttps === 'remoteOnly') { return true; } return lcUrl.startsWith('https://'); } assertUrlNotNullAndCorrectProtocol(url, description) { if (!url) { throw new Error(`'${description}' should not be null`); } if (!this.validateUrlForHttps(url)) { throw new Error(`'${description}' must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS).`); } } validateUrlAgainstIssuer(url) { if (!this.strictDiscoveryDocumentValidation) { return true; } if (!url) { return true; } return url.toLowerCase().startsWith(this.issuer.toLowerCase()); } setupRefreshTimer() { if (typeof window === 'undefined') { this.debug('timer not supported on this plattform'); return; } if (this.hasValidIdToken() || this.hasValidAccessToken()) { this.clearAccessTokenTimer(); this.clearIdTokenTimer(); this.setupExpirationTimers(); } if (this.tokenReceivedSubscription) this.tokenReceivedSubscription.unsubscribe(); this.tokenReceivedSubscription = this.events .pipe(filter((e) => e.type === 'token_received')) .subscribe(() => { this.clearAccessTokenTimer(); this.clearIdTokenTimer(); this.setupExpirationTimers(); }); } setupExpirationTimers() { if (this.hasValidAccessToken()) { this.setupAccessTokenTimer(); } if (!this.disableIdTokenTimer && this.hasValidIdToken()) { this.setupIdTokenTimer(); } } setupAccessTokenTimer() { const expiration = this.getAccessTokenExpiration(); const storedAt = this.getAccessTokenStoredAt(); const timeout = this.calcTimeout(storedAt, expiration); this.ngZone.runOutsideAngular(() => { this.accessTokenTimeoutSubscription = of(new OAuthInfoEvent('token_expires', 'access_token')) .pipe(delay(timeout)) .subscribe((e) => { this.ngZone.run(() => { this.eventsSubject.next(e); }); }); }); } setupIdTokenTimer() { const expiration = this.getIdTokenExpiration(); const storedAt = this.getIdTokenStoredAt(); const timeout = this.calcTimeout(storedAt, expiration); this.ngZone.runOutsideAngular(() => { this.idTokenTimeoutSubscription = of(new OAuthInfoEvent('token_expires', 'id_token')) .pipe(delay(timeout)) .subscribe((e) => { this.ngZone.run(() => { this.eventsSubject.next(e); }); }); }); } /** * Stops timers for automatic refresh. * To restart it, call setupAutomaticSilentRefresh again. */ stopAutomaticRefresh() { this.clearAccessTokenTimer(); this.clearIdTokenTimer(); this.clearAutomaticRefreshTimer(); } clearAccessTokenTimer() { if (this.accessTokenTimeoutSubscription) { this.accessTokenTimeoutSubscription.unsubscribe(); } } clearIdTokenTimer() { if (this.idTokenTimeoutSubscription) { this.idTokenTimeoutSubscription.unsubscribe(); } } clearAutomaticRefreshTimer() { if (this.automaticRefreshSubscription) { this.automaticRefreshSubscription.unsubscribe(); } } calcTimeout(storedAt, expiration) { const now = this.dateTimeService.now(); const delta = (expiration - storedAt) * this.timeoutFactor - (now - storedAt); const duration = Math.max(0, delta); const maxTimeoutValue = 2_147_483_647; return duration > maxTimeoutValue ? maxTimeoutValue : duration; } /** * DEPRECATED. Use a provider for OAuthStorage instead: * * { provide: OAuthStorage, useFactory: oAuthStorageFactory } * export function oAuthStorageFactory(): OAuthStorage { return localStorage; } * Sets a custom storage used to store the received * tokens on client side. By default, the browser's * sessionStorage is used. * @ignore * * @param storage */ setStorage(storage) { this._storage = storage; this.configChanged(); } /** * Loads the discovery document to configure most * properties of this service. The url of the discovery * document is infered from the issuer's url according * to the OpenId Connect spec. To use another url you * can pass it to to optional parameter fullUrl. * * @param fullUrl */ loadDiscoveryDocument(fullUrl = null) { return new Promise((resolve, reject) => { if (!fullUrl) { fullUrl = this.issuer || ''; if (!fullUrl.endsWith('/')) { fullUrl += '/'; } fullUrl += '.well-known/openid-configuration'; } if (!this.validateUrlForHttps(fullUrl)) { reject("issuer must use HTTPS (with TLS), or config value for property 'requireHttps' must be set to 'false' and allow HTTP (without TLS)."); return; } this.http.get(fullUrl).subscribe((doc) => { if (!this.validateDiscoveryDocument(doc)) { this.eventsSubject.next(new OAuthErrorEvent('discovery_document_validation_error', null)); reject('discovery_document_validation_error'); return; } this.loginUrl = doc.authorization_endpoint; this.logoutUrl = doc.end_session_endpoint || this.logoutUrl; this.grantTypesSupported = doc.grant_types_supported; this.issuer = doc.issuer; this.tokenEndpoint = doc.token_endpoint; this.userinfoEndpoint = doc.userinfo_endpoint || this.userinfoEndpoint; this.jwksUri = doc.jwks_uri; this.sessionCheckIFrameUrl = doc.check_session_iframe || this.sessionCheckIFrameUrl; this.discoveryDocumentLoaded = true; this.discoveryDocumentLoadedSubject.next(doc); this.revocationEndpoint = doc.revocation_endpoint || this.revocationEndpoint; if (this.sessionChecksEnabled) { this.restartSessionChecksIfStillLoggedIn(); } this.loadJwks() .then((jwks) => { const result = { discoveryDocument: doc, jwks: jwks, }; const event = new OAuthSuccessEvent('discovery_document_loaded', result); this.eventsSubject.next(event); resolve(event); return; }) .catch((err) => { this.eventsSubject.next(new OAuthErrorEvent('discovery_document_load_error', err)); reject(err); return; }); }, (err) => { this.logger.error('error loading discovery document', err); this.eventsSubject.next(new OAuthErrorEvent('discovery_document_load_error', err)); reject(err); }); }); } loadJwks() { return new Promise((resolve, reject) => { if (this.jwksUri) { this.http.get(this.jwksUri).subscribe((jwks) => { this.jwks = jwks; // this.eventsSubject.next( // new OAuthSuccessEvent('discovery_document_loaded') // ); resolve(jwks); }, (err) => { this.logger.error('error loading jwks', err); this.eventsSubject.next(new OAuthErrorEvent('jwks_load_error', err)); reject(err); }); } else { resolve(null); } }); } validateDiscoveryDocument(doc) { let errors; if (!this.skipIssuerCheck && doc.issuer !== this.issuer) { this.logger.error('invalid issuer in discovery document', 'expected: ' + this.issuer, 'current: ' + doc.issuer); return false; } errors = this.validateUrlFromDiscoveryDocument(doc.authorization_endpoint); if (errors.length > 0) { this.logger.error('error validating authorization_endpoint in discovery document', errors); return false; } errors = this.validateUrlFromDiscoveryDocument(doc.end_session_endpoint); if (errors.length > 0) { this.logger.error('error validating end_session_endpoint in discovery document', errors); return false; } errors = this.validateUrlFromDiscoveryDocument(doc.token_endpoint); if (errors.length > 0) {