UNPKG

angular-oauth2-oidc-codeflow-pkce

Version:

[![Build Status](https://travis-ci.org/bechhansen/angular-oauth2-oidc.svg?branch=master)](https://travis-ci.org/bechhansen/angular-oauth2-oidc)

1,418 lines (1,406 loc) 85.7 kB
import { Injectable, NgZone, Optional, NgModule, InjectionToken } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams, HTTP_INTERCEPTORS } from '@angular/common/http'; import { Subject, of, race, throwError } from 'rxjs'; import { filter, delay, first, tap, map, catchError } from 'rxjs/operators'; import { CommonModule } from '@angular/common'; import { KEYUTIL, KJUR } from 'jsrsasign'; /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ /** * Additional options that can be passt to tryLogin. */ class LoginOptions { constructor() { /** * 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. */ this.preventClearHashAfterLogin = false; } } /** * 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. * @abstract */ class OAuthStorage { } /** * Represents the received tokens, the received state * and the parsed claims from the id-token. */ class ReceivedTokens { } /** * Represents the parsed and validated id_token. * @record */ /** * Represents the response from the token endpoint * http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint * @record */ /** * Represents the response from the user info endpoint * http://openid.net/specs/openid-connect-core-1_0.html#UserInfo * @record */ /** * Represents an OpenID Connect discovery document * @record */ /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ /** * @record */ /** * Interface for Handlers that are hooked in to * validate tokens. * @abstract */ 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. * @abstract */ class AbstractValidationHandler { /** * Validates the at_hash in an id_token against the received access_token. * @param {?} params * @return {?} */ validateAtHash(params) { let /** @type {?} */ hashAlg = this.inferHashAlgorithm(params.idTokenHeader); let /** @type {?} */ tokenHash = this.calcHash(params.accessToken, hashAlg); // sha256(accessToken, { asString: true }); let /** @type {?} */ leftMostHalf = tokenHash.substr(0, tokenHash.length / 2); let /** @type {?} */ tokenHashBase64 = btoa(leftMostHalf); let /** @type {?} */ atHash = tokenHashBase64 .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); let /** @type {?} */ 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 * @return {?} */ inferHashAlgorithm(jwtHeader) { let /** @type {?} */ alg = jwtHeader['alg']; if (!alg.match(/^.S[0-9]{3}$/)) { throw new Error('Algorithm not supported: ' + alg); } return 'sha' + alg.substr(2); } } /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ class UrlHelperService { /** * @param {?=} customHashFragment * @return {?} */ getHashFragmentParams(customHashFragment) { let /** @type {?} */ hash = customHashFragment || window.location.hash; hash = decodeURIComponent(hash); if (hash.indexOf('#') !== 0) { return {}; } const /** @type {?} */ questionMarkPosition = hash.indexOf('?'); if (questionMarkPosition > -1) { hash = hash.substr(questionMarkPosition + 1); } else { hash = hash.substr(1); } return this.parseQueryString(hash); } /** * @param {?} queryString * @return {?} */ parseQueryString(queryString) { const /** @type {?} */ data = {}; let /** @type {?} */ pairs, /** @type {?} */ pair, /** @type {?} */ separatorIndex, /** @type {?} */ escapedKey, /** @type {?} */ escapedValue, /** @type {?} */ key, /** @type {?} */ value; if (queryString === null) { return data; } pairs = queryString.split('&'); for (let /** @type {?} */ 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; } } UrlHelperService.decorators = [ { type: Injectable }, ]; /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ /** * @abstract */ class OAuthEvent { /** * @param {?} type */ constructor(type) { this.type = type; } } class OAuthSuccessEvent extends OAuthEvent { /** * @param {?} type * @param {?=} info */ constructor(type, info = null) { super(type); this.info = info; } } class OAuthInfoEvent extends OAuthEvent { /** * @param {?} type * @param {?=} info */ constructor(type, info = null) { super(type); this.info = info; } } class OAuthErrorEvent extends OAuthEvent { /** * @param {?} type * @param {?} reason * @param {?=} params */ constructor(type, reason, params = null) { super(type); this.reason = reason; this.params = params; } } /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ /** * @param {?} str * @return {?} */ function b64DecodeUnicode(str) { const /** @type {?} */ base64 = str.replace(/\-/g, '+').replace(/\_/g, '/'); return decodeURIComponent(atob(base64) .split('') .map(function (c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }) .join('')); } /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ class AuthConfig { /** * @param {?=} json */ 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 = ''; /** * 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 a 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 custom userinfo endpoint */ this.customUserinfoEndpoint = null; /** * Url of the userinfo endpoint as defined by OpenId Connect. * */ this.userinfoEndpoint = null; this.responseType = 'token'; /** * Defines whether additional debug information should * be shown at the console. */ 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 * depreacted b/c of typo, see 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 secreat while the standards do not * demand for it. In this case, you can set a password * here. As this passwort is exposed to the public * it does not bring additional security and is therefore * as good as using no password. */ this.dummyClientSecret = null; /** * 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; /** * Intervall 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 the we are bypassing * a security check which means we are more vulnerable. */ this.disableAtHashCheck = false; this.skipSubjectCheck = false; this.useIdTokenHintForSilentRefresh = false; this.skipIssuerCheck = false; this.nonceStateSeparator = ';'; this.useHttpBasicAuthForPasswordFlow = false; this.disableNonceCheck = 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); } } } /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ /** * This custom encoder allows charactes like +, % and / to be used in passwords */ class WebHttpUrlEncodingCodec { /** * @param {?} k * @return {?} */ encodeKey(k) { return encodeURIComponent(k); } /** * @param {?} v * @return {?} */ encodeValue(v) { return encodeURIComponent(v); } /** * @param {?} k * @return {?} */ decodeKey(k) { return decodeURIComponent(k); } /** * @param {?} v * @return {?} */ decodeValue(v) { return decodeURIComponent(v); } } /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ /** * Service for logging in and logging out with * OIDC and OAuth2. Supports implicit flow and * password flow. */ class OAuthService extends AuthConfig { /** * @param {?} ngZone * @param {?} http * @param {?} storage * @param {?} tokenValidationHandler * @param {?} config * @param {?} urlHelper */ constructor(ngZone, http, storage, tokenValidationHandler, config, urlHelper) { super(); this.ngZone = ngZone; this.http = http; this.config = config; this.urlHelper = urlHelper; /** * \@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.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 (/** @type {?} */ e) { console.error('cannot access sessionStorage. Consider setting an own storage implementation using setStorage', e); } this.setupRefreshTimer(); } /** * Use this method to configure the service * @param {?} config the configuration * @return {?} */ configure(config) { // For the sake of downward compatibility with // original configuration API Object.assign(this, new AuthConfig(), config); this.config = Object.assign(/** @type {?} */ ({}), new AuthConfig(), config); if (this.sessionChecksEnabled) { this.setupSessionCheck(); } this.configChanged(); } /** * @return {?} */ configChanged() { } /** * @return {?} */ restartSessionChecksIfStillLoggedIn() { if (this.hasValidIdToken()) { this.initSessionCheck(); } } /** * @return {?} */ restartRefreshTimerIfStillLoggedIn() { this.setupExpirationTimers(); } /** * @return {?} */ setupSessionCheck() { this.events.pipe(filter(e => e.type === 'token_received')).subscribe(e => { this.initSessionCheck(); }); } /** * * @param {?=} params Additional parameter to pass * @return {?} */ setupAutomaticSilentRefresh(params = {}) { this.events.pipe(filter(e => e.type === 'token_expires')).subscribe(e => { this.silentRefresh(params).catch(_ => { this.debug('automatic silent refresh did not work'); }); }); this.restartRefreshTimerIfStillLoggedIn(); } /** * @param {?=} options * @return {?} */ loadDiscoveryDocumentAndTryLogin(options = null) { return this.loadDiscoveryDocument().then(doc => { return this.tryLogin(options); }); } /** * @param {?=} options * @return {?} */ loadDiscoveryDocumentAndLogin(options = null) { return this.loadDiscoveryDocumentAndTryLogin(options).then(_ => { if (!this.hasValidIdToken() || !this.hasValidAccessToken()) { this.initImplicitFlow(); return false; } else { return true; } }); } /** * @param {...?} args * @return {?} */ debug(...args) { if (this.showDebugInformation) { console.debug.apply(console, args); } } /** * @param {?} url * @return {?} */ validateUrlFromDiscoveryDocument(url) { const /** @type {?} */ errors = []; const /** @type {?} */ httpsCheck = this.validateUrlForHttps(url); const /** @type {?} */ 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; } /** * @param {?} url * @return {?} */ validateUrlForHttps(url) { if (!url) { return true; } const /** @type {?} */ 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://'); } /** * @param {?} url * @return {?} */ validateUrlAgainstIssuer(url) { if (!this.strictDiscoveryDocumentValidation) { return true; } if (!url) { return true; } return url.toLowerCase().startsWith(this.issuer.toLowerCase()); } /** * @return {?} */ setupRefreshTimer() { if (typeof window === 'undefined') { this.debug('timer not supported on this plattform'); return; } if (this.hasValidIdToken()) { this.clearAccessTokenTimer(); this.clearIdTokenTimer(); this.setupExpirationTimers(); } this.events.pipe(filter(e => e.type === 'token_received')).subscribe(_ => { this.clearAccessTokenTimer(); this.clearIdTokenTimer(); this.setupExpirationTimers(); }); } /** * @return {?} */ setupExpirationTimers() { const /** @type {?} */ idTokenExp = this.getIdTokenExpiration() || Number.MAX_VALUE; const /** @type {?} */ accessTokenExp = this.getAccessTokenExpiration() || Number.MAX_VALUE; const /** @type {?} */ useAccessTokenExp = accessTokenExp <= idTokenExp; if (this.hasValidAccessToken() && useAccessTokenExp) { this.setupAccessTokenTimer(); } if (this.hasValidIdToken() && !useAccessTokenExp) { this.setupIdTokenTimer(); } } /** * @return {?} */ setupAccessTokenTimer() { const /** @type {?} */ expiration = this.getAccessTokenExpiration(); const /** @type {?} */ storedAt = this.getAccessTokenStoredAt(); const /** @type {?} */ 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); }); }); }); } /** * @return {?} */ setupIdTokenTimer() { const /** @type {?} */ expiration = this.getIdTokenExpiration(); const /** @type {?} */ storedAt = this.getIdTokenStoredAt(); const /** @type {?} */ 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); }); }); }); } /** * @return {?} */ clearAccessTokenTimer() { if (this.accessTokenTimeoutSubscription) { this.accessTokenTimeoutSubscription.unsubscribe(); } } /** * @return {?} */ clearIdTokenTimer() { if (this.idTokenTimeoutSubscription) { this.idTokenTimeoutSubscription.unsubscribe(); } } /** * @param {?} storedAt * @param {?} expiration * @return {?} */ calcTimeout(storedAt, expiration) { const /** @type {?} */ delta = (expiration - storedAt) * this.timeoutFactor; return delta; } /** * Sets a custom storage used to store the received * tokens on client side. By default, the browser's * sessionStorage is used. * * @param {?} storage * @return {?} */ 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 * @return {?} */ 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. Also check property requireHttps.'); 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.jwksUri = doc.jwks_uri; this.sessionCheckIFrameUrl = doc.check_session_iframe || this.sessionCheckIFrameUrl; this.discoveryDocumentLoaded = true; this.discoveryDocumentLoadedSubject.next(doc); if (this.sessionChecksEnabled) { this.restartSessionChecksIfStillLoggedIn(); } this.loadJwks() .then(jwks => { const /** @type {?} */ result = { discoveryDocument: doc, jwks: jwks }; const /** @type {?} */ 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 => { console.error('error loading discovery document', err); this.eventsSubject.next(new OAuthErrorEvent('discovery_document_load_error', err)); reject(err); }); }); } /** * @return {?} */ 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 => { console.error('error loading jwks', err); this.eventsSubject.next(new OAuthErrorEvent('jwks_load_error', err)); reject(err); }); } else { resolve(null); } }); } /** * @param {?} doc * @return {?} */ validateDiscoveryDocument(doc) { let /** @type {?} */ errors; if (!this.skipIssuerCheck && doc.issuer !== this.issuer) { console.error('invalid issuer in discovery document', 'expected: ' + this.issuer, 'current: ' + doc.issuer); return false; } errors = this.validateUrlFromDiscoveryDocument(doc.authorization_endpoint); if (errors.length > 0) { console.error('error validating authorization_endpoint in discovery document', errors); return false; } errors = this.validateUrlFromDiscoveryDocument(doc.end_session_endpoint); if (errors.length > 0) { console.error('error validating end_session_endpoint in discovery document', errors); return false; } errors = this.validateUrlFromDiscoveryDocument(doc.token_endpoint); if (errors.length > 0) { console.error('error validating token_endpoint in discovery document', errors); } errors = this.validateUrlFromDiscoveryDocument(doc.userinfo_endpoint); if (errors.length > 0) { console.error('error validating userinfo_endpoint in discovery document', errors); return false; } errors = this.validateUrlFromDiscoveryDocument(doc.jwks_uri); if (errors.length > 0) { console.error('error validating jwks_uri in discovery document', errors); return false; } if (this.sessionChecksEnabled && !doc.check_session_iframe) { console.warn('sessionChecksEnabled is activated but discovery document' + ' does not contain a check_session_iframe field'); } // this.sessionChecksEnabled = !!doc.check_session_iframe; return true; } /** * Uses password flow to exchange userName and password for an * access_token. After receiving the access_token, this method * uses it to query the userinfo endpoint in order to get information * about the user in question. * * When using this, make sure that the property oidc is set to false. * Otherwise stricter validations take happen that makes this operation * fail. * * @param {?} userName * @param {?} password * @param {?=} headers Optional additional http-headers. * @return {?} */ fetchTokenUsingPasswordFlowAndLoadUserProfile(userName, password, headers = new HttpHeaders()) { return this.fetchTokenUsingPasswordFlow(userName, password, headers).then(() => this.loadUserProfile()); } /** * Loads the user profile by accessing the user info endpoint defined by OpenId Connect. * * When using this with OAuth2 password flow, make sure that the property oidc is set to false. * Otherwise stricter validations take happen that makes this operation * fail. * @return {?} */ loadUserProfile() { if (!this.hasValidAccessToken()) { throw new Error('Can not load User Profile without access_token'); } const /** @type {?} */ userinfoEndpoint = this.customUserinfoEndpoint ? this.customUserinfoEndpoint : this.userinfoEndpoint; if (!this.validateUrlForHttps(userinfoEndpoint)) { throw new Error('userinfoEndpoint must use Http. Also check property requireHttps.'); } const /** @type {?} */ customBearer = this.customUserinfoEndpoint ? ('Bearer ' + this.getIdToken()) : ('Bearer ' + this.getAccessToken()); return new Promise((resolve, reject) => { const /** @type {?} */ headers = new HttpHeaders().set('Authorization', customBearer); this.http.get(userinfoEndpoint, { headers }).subscribe(info => { this.debug('userinfo received', info); const /** @type {?} */ existingClaims = this.getIdentityClaims() || {}; if (!this.skipSubjectCheck) { if (this.oidc && (!existingClaims['sub'] || info.sub !== existingClaims['sub'])) { const /** @type {?} */ err = 'if property oidc is true, the received user-id (sub) has to be the user-id ' + 'of the user that has logged in with oidc.\n' + 'if you are not using oidc but just oauth2 password flow set oidc to false'; reject(err); return; } } info = Object.assign({}, existingClaims, info); this._storage.setItem('id_token_claims_obj', JSON.stringify(info)); this.eventsSubject.next(new OAuthSuccessEvent('user_profile_loaded')); resolve(info); }, err => { console.error('error loading user info', err); this.eventsSubject.next(new OAuthErrorEvent('user_profile_load_error', err)); reject(err); }); }); } /** * Uses password flow to exchange userName and password for an access_token. * @param {?} userName * @param {?} password * @param {?=} headers Optional additional http-headers. * @return {?} */ fetchTokenUsingPasswordFlow(userName, password, headers = new HttpHeaders()) { if (!this.validateUrlForHttps(this.tokenEndpoint)) { throw new Error('tokenEndpoint must use Http. Also check property requireHttps.'); } return new Promise((resolve, reject) => { /** * A `HttpParameterCodec` that uses `encodeURIComponent` and `decodeURIComponent` to * serialize and parse URL parameter keys and values. * * \@stable */ let /** @type {?} */ params = new HttpParams({ encoder: new WebHttpUrlEncodingCodec() }) .set('grant_type', 'password') .set('scope', this.scope) .set('username', userName) .set('password', password); if (this.useHttpBasicAuthForPasswordFlow) { const /** @type {?} */ header = btoa(`${this.clientId}:${this.dummyClientSecret}`); headers = headers.set('Authentication', 'BASIC ' + header); } if (!this.useHttpBasicAuthForPasswordFlow) { params = params.set('client_id', this.clientId); } if (!this.useHttpBasicAuthForPasswordFlow && this.dummyClientSecret) { params = params.set('client_secret', this.dummyClientSecret); } if (this.customQueryParams) { for (const /** @type {?} */ key of Object.getOwnPropertyNames(this.customQueryParams)) { params = params.set(key, this.customQueryParams[key]); } } headers = headers.set('Content-Type', 'application/x-www-form-urlencoded'); this.http .post(this.tokenEndpoint, params, { headers }) .subscribe(tokenResponse => { this.debug('tokenResponse', tokenResponse); this.storeAccessTokenResponse(tokenResponse.access_token, tokenResponse.refresh_token, tokenResponse.expires_in, tokenResponse.scope); this.eventsSubject.next(new OAuthSuccessEvent('token_received')); resolve(tokenResponse); }, err => { console.error('Error performing password flow', err); this.eventsSubject.next(new OAuthErrorEvent('token_error', err)); reject(err); }); }); } /** * Refreshes the token using a refresh_token. * This does not work for implicit flow, b/c * there is no refresh_token in this flow. * A solution for this is provided by the * method silentRefresh. * @return {?} */ refreshToken() { let /** @type {?} */ params = new HttpParams() .set('grant_type', 'refresh_token') .set('refresh_token', this._storage.getItem('refresh_token')) .set('scope', this.scope); const /** @type {?} */ savedNonce = this._storage.getItem('nonce'); if (savedNonce && this.oidc) { params = params.set('nonce', savedNonce); } if (this.dummyClientSecret) { params = params.set('client_secret', this.dummyClientSecret); } return this.fetchToken(params); } /** * Get token using an intermediate code. Works for the Authorization Code flow. * @param {?} code * @return {?} */ getTokenFromCode(code) { const /** @type {?} */ params = new HttpParams() .set('grant_type', 'authorization_code') .set('code', code) .set('redirect_uri', this.redirectUri); return this.fetchToken(params); } /** * @param {?} params * @return {?} */ fetchToken(params) { if (!this.validateUrlForHttps(this.tokenEndpoint)) { throw new Error('tokenEndpoint must use Http. Also check property requireHttps.'); } return new Promise((resolve, reject) => { params = params.set('client_id', this.clientId); if (this.customQueryParams) { for (const /** @type {?} */ key of Object.getOwnPropertyNames(this.customQueryParams)) { params = params.set(key, this.customQueryParams[key]); } } const /** @type {?} */ headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded'); this.http.post(this.tokenEndpoint, params, { headers }).subscribe((tokenResponse) => { this.debug('refresh tokenResponse', tokenResponse); this.storeAccessTokenResponse(tokenResponse.access_token, tokenResponse.refresh_token, tokenResponse.expires_in, tokenResponse.scope); if (this.oidc && tokenResponse.id_token) { this.processIdToken(tokenResponse.id_token, tokenResponse.access_token).then(result => { this.storeIdToken(result); this.eventsSubject.next(new OAuthSuccessEvent('token_received')); this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed')); resolve(tokenResponse); }) .catch(reason => { this.eventsSubject.next(new OAuthErrorEvent('token_validation_error', reason)); console.error('Error validating tokens'); console.error(reason); reject(reason); }); } else { this.eventsSubject.next(new OAuthSuccessEvent('token_received')); this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed')); resolve(tokenResponse); } }, (err) => { console.error('Error getting token', err); this.eventsSubject.next(new OAuthErrorEvent('token_refresh_error', err)); reject(err); }); }); } /** * @return {?} */ removeSilentRefreshEventListener() { if (this.silentRefreshPostMessageEventListener) { window.removeEventListener('message', this.silentRefreshPostMessageEventListener); this.silentRefreshPostMessageEventListener = null; } } /** * @return {?} */ setupSilentRefreshEventListener() { this.removeSilentRefreshEventListener(); this.silentRefreshPostMessageEventListener = (e) => { let /** @type {?} */ expectedPrefix = '#'; if (this.silentRefreshMessagePrefix) { expectedPrefix += this.silentRefreshMessagePrefix; } if (!e || !e.data || typeof e.data !== 'string') { return; } const /** @type {?} */ prefixedMessage = e.data; if (!prefixedMessage.startsWith(expectedPrefix)) { return; } const /** @type {?} */ message = '#' + prefixedMessage.substr(expectedPrefix.length); this.tryLogin({ customHashFragment: message, preventClearHashAfterLogin: true, onLoginError: err => { this.eventsSubject.next(new OAuthErrorEvent('silent_refresh_error', err)); }, onTokenReceived: () => { this.eventsSubject.next(new OAuthSuccessEvent('silently_refreshed')); } }).catch(err => this.debug('tryLogin during silent refresh failed', err)); }; window.addEventListener('message', this.silentRefreshPostMessageEventListener); } /** * Performs a silent refresh for implicit flow. * Use this method to get a new tokens when/ before * the existing tokens expires. * @param {?=} params * @param {?=} noPrompt * @return {?} */ silentRefresh(params = {}, noPrompt = true) { const /** @type {?} */ claims = this.getIdentityClaims() || {}; if (this.useIdTokenHintForSilentRefresh && this.hasValidIdToken()) { params['id_token_hint'] = this.getIdToken(); } /* if (!claims) { throw new Error('cannot perform a silent refresh as the user is not logged in'); } */ if (!this.validateUrlForHttps(this.loginUrl)) { throw new Error('tokenEndpoint must use Https. Also check property requireHttps.'); } if (typeof document === 'undefined') { throw new Error('silent refresh is not supported on this platform'); } const /** @type {?} */ existingIframe = document.getElementById(this.silentRefreshIFrameName); if (existingIframe) { document.body.removeChild(existingIframe); } this.silentRefreshSubject = claims['sub']; const /** @type {?} */ iframe = document.createElement('iframe'); iframe.id = this.silentRefreshIFrameName; this.setupSilentRefreshEventListener(); const /** @type {?} */ redirectUri = this.silentRefreshRedirectUri || this.redirectUri; this.createLoginUrl(null, null, redirectUri, noPrompt, params).then(url => { iframe.setAttribute('src', url); if (!this.silentRefreshShowIFrame) { iframe.style['display'] = 'none'; } document.body.appendChild(iframe); }); const /** @type {?} */ errors = this.events.pipe(filter(e => e instanceof OAuthErrorEvent), first()); const /** @type {?} */ success = this.events.pipe(filter(e => e.type === 'silently_refreshed'), first()); const /** @type {?} */ timeout = of(new OAuthErrorEvent('silent_refresh_timeout', null)).pipe(delay(this.silentRefreshTimeout)); return race([errors, success, timeout]) .pipe(tap(e => { if (e.type === 'silent_refresh_timeout') { this.eventsSubject.next(e); } }), map(e => { if (e instanceof OAuthErrorEvent) { throw e; } return e; })) .toPromise(); } /** * @return {?} */ canPerformSessionCheck() { if (!this.sessionChecksEnabled) { return false; } if (!this.sessionCheckIFrameUrl) { console.warn('sessionChecksEnabled is activated but there ' + 'is no sessionCheckIFrameUrl'); return false; } const /** @type {?} */ sessionState = this.getSessionState(); if (!sessionState) { console.warn('sessionChecksEnabled is activated but there ' + 'is no session_state'); return false; } if (typeof document === 'undefined') { return false; } return true; } /** * @return {?} */ setupSessionCheckEventListener() { this.removeSessionCheckEventListener(); this.sessionCheckEventListener = (e) => { const /** @type {?} */ origin = e.origin.toLowerCase(); const /** @type {?} */ issuer = this.issuer.toLowerCase(); this.debug('sessionCheckEventListener'); if (!issuer.startsWith(origin)) { this.debug('sessionCheckEventListener', 'wrong origin', origin, 'expected', issuer); } switch (e.data) { case 'unchanged': this.handleSessionUnchanged(); break; case 'changed': this.handleSessionChange(); break; case 'error': this.handleSessionError(); break; } this.debug('got info from session check inframe', e); }; window.addEventListener('message', this.sessionCheckEventListener); } /** * @return {?} */ handleSessionUnchanged() { this.debug('session check', 'session unchanged'); } /** * @return {?} */ handleSessionChange() { /* events: session_changed, relogin, stopTimer, logged_out*/ this.eventsSubject.next(new OAuthInfoEvent('session_changed')); this.stopSessionCheckTimer(); if (this.silentRefreshRedirectUri) { this.silentRefresh().catch(_ => this.debug('silent refresh failed after session changed')); this.waitForSilentRefreshAfterSessionChange(); } else { this.eventsSubject.next(new OAuthInfoEvent('session_terminated')); this.logOut(true); } } /** * @return {?} */ waitForSilentRefreshAfterSessionChange() { this.events .pipe(filter((e) => e.type === 'silently_refreshed' || e.type === 'silent_refresh_timeout' || e.type === 'silent_refresh_error'), first()) .subscribe(e => { if (e.type !== 'silently_refreshed') { this.debug('silent refresh did not work after session changed'); this.eventsSubject.next(new OAuthInfoEvent('session_terminated')); this.logOut(true); } }); } /** * @return {?} */ handleSessionError() { this.stopSessionCheckTimer(); this.eventsSubject.next(new OAuthInfoEvent('session_error')); } /** * @return {?} */ removeSessionCheckEventListener() { if (this.sessionCheckEventListener) { window.removeEventListener('message', this.sessionCheckEventListener); this.sessionCheckEventListener = null; } } /** * @return {?} */ initSessionCheck() { if (!this.canPerformSessionCheck()) { return; } const /** @type {?} */ existingIframe = document.getElementById(this.sessionCheckIFrameName); if (existingIframe) { document.body.removeChild(existingIframe); } const /** @type {?} */ iframe = document.createElement('iframe'); iframe.id = this.sessionCheckIFrameName; this.setupSessionCheckEventListener(); const /** @type {?} */ url = this.sessionCheckIFrameUrl; iframe.setAttribute('src', url); // iframe.style.visibility = 'hidden'; iframe.style.display = 'none'; document.body.appendChild(iframe); this.startSessionCheckTimer(); } /** * @return {?} */ startSessionCheckTimer() { this.stopSessionCheckTimer(); this.sessionCheckTimer = setInterval(this.checkSession.bind(this), this.sessionCheckIntervall); } /** * @return {?} */ stopSessionCheckTimer() { if (this.sessionCheckTimer) { clearInterval(this.sessionCheckTimer); this.sessionCheckTimer = null; } } /** * @return {?} */ checkSession() { const /** @type {?} */ iframe = document.getElementById(this.sessionCheckIFrameName); if (!iframe) { console.warn('checkSession did not find iframe', this.sessionCheckIFrameName); } const /** @type {?} */ sessionState = this.getSessionState(); if (!sessionState) { this.stopSessionCheckTimer(); } const /** @type {?} */ message = this.clientId + ' ' + sessionState; iframe.contentWindow.postMessage(message, this.issuer); } /** * @param {?=} state * @param {?=} loginHint * @param {?=} customRedirectUri * @param {?=} noPrompt * @param {?=} params * @return {?} */ createLoginUrl(state = '', loginHint = '', customRedirectUri = '', noPrompt = false, params = {}) { const /** @type {?} */ that = this; let /** @type {?} */ redirectUri; if (customRedirectUri) { redirectUri = customRedirectUri; } else { redirectUri = this.redirectUri; } let /** @type {?} */ nonce = null; if (!this.disableNonceCheck) { nonce = this.createAndSaveNonce(); if (state) { state = nonce + this.config.nonceStateSeparator + state; } else { state = nonce; } } if (!this.requestAccessToken && !this.oidc) { throw new Error('Either requestAccessToken or oidc or both must be true'); } this.responseType = this.getResponseType(this.inImplicitFlow); const /** @type {?} */ seperationChar = that.loginUrl.indexOf('?') > -1 ? '&' : '?'; let /** @type {?} */ scope = that.scope; if (this.oidc && !scope.match(/(^|\s)openid($|\s)/)) { scope = 'openid ' + scope; } let /** @type {?} */ url = that.loginUrl + seperationChar + 'response_type=' + encodeURIComponent(that.responseType) + '&client_id=' + encodeURIComponent(that.clientId) + '&state=' + encodeURIComponent(state) + '&redirect_uri=' + encodeURIComponent(redirectUri) + '&scope=' + encodeURIComponent(scope); if (loginHint) { url += '&login_hint=' + encodeURIComponent(loginHint); } if (that.resource) { url += '&resource=' + encodeURIComponent(that.resource); } if (nonce && this.oidc) { url += '&nonce=' + encodeURIComponent(nonce); } if (noPrompt) { url += '&prompt=none'; } for (const /** @type {?} */ key of Object.keys(params)) { url += '&' + encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); } if (this.customQueryParams) { for (const /** @type {?} */ key of Object.getOwnPropertyNames(this.customQueryParams)) { url += '&' + key + '=' + encodeURIComponent(this.customQueryParams[key]); } } return Promise.resolve(url); } /** * @param