angular-oauth2-oidc-codeflow-pkce
Version:
[](https://travis-ci.org/bechhansen/angular-oauth2-oidc)
1,418 lines (1,406 loc) • 85.7 kB
JavaScript
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