angular-oauth2-oidc
Version:
Support for OAuth 2(.1) and OpenId Connect (OIDC) in Angular
1,399 lines (1,385 loc) • 126 kB
JavaScript
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) {