UNPKG

angular-oauth2-oidc

Version:

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

1,446 lines (1,433 loc) 85.6 kB
import { __awaiter } from 'tslib'; 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,extraRequire,uselessCode} 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 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. * @abstract */ 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. * @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,extraRequire,uselessCode} 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) { return __awaiter(this, void 0, void 0, function* () { /** @type {?} */ let hashAlg = this.inferHashAlgorithm(params.idTokenHeader); /** @type {?} */ let tokenHash = yield this.calcHash(params.accessToken, hashAlg); // sha256(accessToken, { asString: true }); /** @type {?} */ let leftMostHalf = tokenHash.substr(0, tokenHash.length / 2); /** @type {?} */ let tokenHashBase64 = btoa(leftMostHalf); /** @type {?} */ let atHash = tokenHashBase64 .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); /** @type {?} */ let 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) { /** @type {?} */ let 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,extraRequire,uselessCode} checked by tsc */ class UrlHelperService { /** * @param {?=} customHashFragment * @return {?} */ getHashFragmentParams(customHashFragment) { /** @type {?} */ let hash = customHashFragment || window.location.hash; hash = decodeURIComponent(hash); if (hash.indexOf('#') !== 0) { return {}; } /** @type {?} */ const 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) { /** @type {?} */ const data = {}; /** @type {?} */ let pairs; /** @type {?} */ let pair; /** @type {?} */ let separatorIndex; /** @type {?} */ let escapedKey; /** @type {?} */ let escapedValue; /** @type {?} */ let key; /** @type {?} */ let value; if (queryString === null) { return data; } 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; } } UrlHelperService.decorators = [ { type: Injectable }, ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} 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,extraRequire,uselessCode} checked by tsc */ // see: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_.22Unicode_Problem.22 /** * @param {?} str * @return {?} */ function b64DecodeUnicode(str) { /** @type {?} */ 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('')); } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,uselessCode} 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 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 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 * 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 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 = 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; /** * 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 the 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 password flow */ this.useHttpBasicAuthForPasswordFlow = 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,extraRequire,uselessCode} 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,extraRequire,uselessCode} 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 * @param {?} logger */ constructor(ngZone, http, storage, tokenValidationHandler, config, urlHelper, logger) { super(); this.ngZone = ngZone; this.http = http; this.config = config; this.urlHelper = urlHelper; this.logger = logger; /** * \@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 (e) { console.error('No OAuthStorage provided and cannot access default (sessionStorage).' + 'Consider providing a custom OAuthStorage implementation in your module.', 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(); }); } /** * Will setup up silent refreshing for when the token is * about to expire. * @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(); } /** * 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(...)` * @return {?} */ loadDiscoveryDocumentAndTryLogin(options = null) { return this.loadDiscoveryDocument().then(doc => { return this.tryLogin(options); }); } /** * Convenience method that first calls `loadDiscoveryDocumentAndTryLogin(...)` * and if then chains to `initImplicitFlow()`, but only if there is no valid * IdToken or no valid AccessToken. * * @param {?=} options LoginOptions to pass through to `tryLogin(...)` * @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) { this.logger.debug.apply(console, args); } } /** * @param {?} url * @return {?} */ validateUrlFromDiscoveryDocument(url) { /** @type {?} */ const errors = []; /** @type {?} */ const httpsCheck = this.validateUrlForHttps(url); /** @type {?} */ 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; } /** * @param {?} url * @return {?} */ validateUrlForHttps(url) { if (!url) { return true; } /** @type {?} */ 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://'); } /** * @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() { /** @type {?} */ const idTokenExp = this.getIdTokenExpiration() || Number.MAX_VALUE; /** @type {?} */ const accessTokenExp = this.getAccessTokenExpiration() || Number.MAX_VALUE; /** @type {?} */ const useAccessTokenExp = accessTokenExp <= idTokenExp; if (this.hasValidAccessToken() && useAccessTokenExp) { this.setupAccessTokenTimer(); } if (this.hasValidIdToken() && !useAccessTokenExp) { this.setupIdTokenTimer(); } } /** * @return {?} */ setupAccessTokenTimer() { /** @type {?} */ const expiration = this.getAccessTokenExpiration(); /** @type {?} */ const storedAt = this.getAccessTokenStoredAt(); /** @type {?} */ 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); }); }); }); } /** * @return {?} */ setupIdTokenTimer() { /** @type {?} */ const expiration = this.getIdTokenExpiration(); /** @type {?} */ const storedAt = this.getIdTokenStoredAt(); /** @type {?} */ 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); }); }); }); } /** * @return {?} */ clearAccessTokenTimer() { if (this.accessTokenTimeoutSubscription) { this.accessTokenTimeoutSubscription.unsubscribe(); } } /** * @return {?} */ clearIdTokenTimer() { if (this.idTokenTimeoutSubscription) { this.idTokenTimeoutSubscription.unsubscribe(); } } /** * @param {?} storedAt * @param {?} expiration * @return {?} */ calcTimeout(storedAt, expiration) { /** @type {?} */ const delta = (expiration - storedAt) * this.timeoutFactor; return delta; } /** * DEPRECATED. Use a provider for OAuthStorage instead: * * { provide: OAuthStorage, useValue: 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 * @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, or config value for property requireHttps must allow http'); 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 => { /** @type {?} */ const result = { discoveryDocument: doc, jwks: jwks }; /** @type {?} */ 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); }); }); } /** * @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 => { this.logger.error('error loading jwks', err); this.eventsSubject.next(new OAuthErrorEvent('jwks_load_error', err)); reject(err); }); } else { resolve(null); } }); } /** * @param {?} doc * @return {?} */ validateDiscoveryDocument(doc) { /** @type {?} */ 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) { this.logger.error('error validating token_endpoint in discovery document', errors); } errors = this.validateUrlFromDiscoveryDocument(doc.userinfo_endpoint); if (errors.length > 0) { this.logger.error('error validating userinfo_endpoint in discovery document', errors); return false; } errors = this.validateUrlFromDiscoveryDocument(doc.jwks_uri); if (errors.length > 0) { this.logger.error('error validating jwks_uri in discovery document', errors); return false; } if (this.sessionChecksEnabled && !doc.check_session_iframe) { this.logger.warn('sessionChecksEnabled is activated but discovery document' + ' does not contain a check_session_iframe field'); } 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 place that make 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 place that make this operation fail. * @return {?} */ loadUserProfile() { if (!this.hasValidAccessToken()) { throw new Error('Can not load User Profile without access_token'); } if (!this.validateUrlForHttps(this.userinfoEndpoint)) { throw new Error('userinfoEndpoint must use http, or config value for property requireHttps must allow http'); } return new Promise((resolve, reject) => { /** @type {?} */ const headers = new HttpHeaders().set('Authorization', 'Bearer ' + this.getAccessToken()); this.http.get(this.userinfoEndpoint, { headers }).subscribe(info => { this.debug('userinfo received', info); /** @type {?} */ const existingClaims = this.getIdentityClaims() || {}; if (!this.skipSubjectCheck) { if (this.oidc && (!existingClaims['sub'] || info.sub !== existingClaims['sub'])) { /** @type {?} */ const 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 => { this.logger.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, or config value for property requireHttps must allow http'); } return new Promise((resolve, reject) => { /** * A `HttpParameterCodec` that uses `encodeURIComponent` and `decodeURIComponent` to * serialize and parse URL parameter keys and values. * * \@stable * @type {?} */ let params = new HttpParams({ encoder: new WebHttpUrlEncodingCodec() }) .set('grant_type', 'password') .set('scope', this.scope) .set('username', userName) .set('password', password); if (this.useHttpBasicAuthForPasswordFlow) { /** @type {?} */ const header = btoa(`${this.clientId}:${this.dummyClientSecret}`); headers = headers.set('Authorization', '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 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 => { this.logger.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() { if (!this.validateUrlForHttps(this.tokenEndpoint)) { throw new Error('tokenEndpoint must use http, or config value for property requireHttps must allow http'); } return new Promise((resolve, reject) => { /** @type {?} */ let params = new HttpParams() .set('grant_type', 'refresh_token') .set('client_id', this.clientId) .set('scope', this.scope) .set('refresh_token', this._storage.getItem('refresh_token')); if (this.dummyClientSecret) { params = params.set('client_secret', this.dummyClientSecret); } if (this.customQueryParams) { for (const key of Object.getOwnPropertyNames(this.customQueryParams)) { params = params.set(key, this.customQueryParams[key]); } } /** @type {?} */ const 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); this.eventsSubject.next(new OAuthSuccessEvent('token_received')); this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed')); resolve(tokenResponse); }, err => { this.logger.error('Error performing password flow', 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) => { /** @type {?} */ let expectedPrefix = '#'; if (this.silentRefreshMessagePrefix) { expectedPrefix += this.silentRefreshMessagePrefix; } if (!e || !e.data || typeof e.data !== 'string') { return; } /** @type {?} */ const prefixedMessage = e.data; if (!prefixedMessage.startsWith(expectedPrefix)) { return; } /** @type {?} */ const 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 new tokens when/before * the existing tokens expire. * @param {?=} params * @param {?=} noPrompt * @return {?} */ silentRefresh(params = {}, noPrompt = true) { /** @type {?} */ const claims = this.getIdentityClaims() || {}; if (this.useIdTokenHintForSilentRefresh && this.hasValidIdToken()) { params['id_token_hint'] = this.getIdToken(); } if (!this.validateUrlForHttps(this.loginUrl)) { throw new Error('tokenEndpoint must use https, or config value for property requireHttps must allow http'); } if (typeof document === 'undefined') { throw new Error('silent refresh is not supported on this platform'); } /** @type {?} */ const existingIframe = document.getElementById(this.silentRefreshIFrameName); if (existingIframe) { document.body.removeChild(existingIframe); } this.silentRefreshSubject = claims['sub']; /** @type {?} */ const iframe = document.createElement('iframe'); iframe.id = this.silentRefreshIFrameName; this.setupSilentRefreshEventListener(); /** @type {?} */ const 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); }); /** @type {?} */ const errors = this.events.pipe(filter(e => e instanceof OAuthErrorEvent), first()); /** @type {?} */ const success = this.events.pipe(filter(e => e.type === 'silently_refreshed'), first()); /** @type {?} */ const 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; } /** @type {?} */ const 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) => { /** @type {?} */ const origin = e.origin.toLowerCase(); /** @type {?} */ const issuer = this.issuer.toLowerCase(); this.debug('sessionCheckEventListener'); if (!issuer.startsWith(origin)) { this.debug('sessionCheckEventListener', 'wrong origin', origin, 'expected', issuer); } // only run in Angular zone if it is 'changed' or 'error' switch (e.data) { case 'unchanged': this.handleSessionUnchanged(); break; case 'changed': this.ngZone.run(() => { this.handleSessionChange(); }); break; case 'error': this.ngZone.run(() => { this.handleSessionError(); }); break; } this.debug('got info from session check inframe', e); }; // prevent Angular from refreshing the view on every message (runs in intervals) this.ngZone.runOutsideAngular(() => { 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; } /** @type {?} */ const existingIframe = document.getElementById(this.sessionCheckIFrameName); if (existingIframe) { document.body.removeChild(existingIframe); } /** @type {?} */ const iframe = document.createElement('iframe'); iframe.id = this.sessionCheckIFrameName; this.setupSessionCheckEventListener(); /** @type {?} */ const url = this.sessionCheckIFrameUrl; iframe.setAttribute('src', url); iframe.style.display = 'none'; document.body.appendChild(iframe); this.startSessionCheckTimer(); } /** * @return {?} */ startSessionCheckTimer() { this.stopSessionCheckTimer(); this.ngZone.runOutsideAngular(() => { this.sessionCheckTimer = setInterval(this.checkSession.bind(this), this.sessionCheckIntervall); }); } /** * @return {?} */ stopSessionCheckTimer() { if (this.sessionCheckTimer) { clearInterval(this.sessionCheckTimer); this.sessionCheckTimer = null; } } /** * @return {?} */ checkSession() { /** @type {?} */ const iframe = document.getElementById(this.sessionCheckIFrameName); if (!iframe) { this.logger.warn('checkSession did not find iframe', this.sessionCheckIFrameName); } /** @type {?} */ const sessionState = this.getSessionState(); if (!sessionState) { this.stopSessionCheckTimer(); } /** @type {?} */ const 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 = {}) { /** @type {?} */ const that = this; /** @type {?} */ let redirectUri; if (customRedirectUri) { redirectUri = customRedirectUri; } else { redirectUri = this.redirectUri; } return this.createAndSaveNonce().then((nonce) => { 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'); } if (this.config.responseType) { this.responseType = this.config.responseType; } else { if (this.oidc && this.requestAccessToken) { this.responseType = 'id_token token'; } else if (this.oidc && !this.requestAccessToken) {