// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. import { Log } from './Log.js'; import { OidcClient } from './OidcClient.js'; import { UserManagerSettings } from './UserManagerSettings.js'; import { User } from './User.js'; import { UserManagerEvents } from './UserManagerEvents.js'; import { SilentRenewService } from './SilentRenewService.js'; import { SessionMonitor } from './SessionMonitor.js'; import { SigninRequest } from "./SigninRequest"; import { TokenRevocationClient } from './TokenRevocationClient.js'; import { TokenClient } from './TokenClient.js'; import { JoseUtil } from './JoseUtil.js'; export class UserManager extends OidcClient { constructor(settings = {}, SilentRenewServiceCtor = SilentRenewService, SessionMonitorCtor = SessionMonitor, TokenRevocationClientCtor = TokenRevocationClient, TokenClientCtor = TokenClient, joseUtil = JoseUtil ) { if (!(settings instanceof UserManagerSettings)) { settings = new UserManagerSettings(settings); } super(settings); this._events = new UserManagerEvents(settings); this._silentRenewService = new SilentRenewServiceCtor(this); // order is important for the following properties; these services depend upon the events. if (this.settings.automaticSilentRenew) { Log.debug("UserManager.ctor: automaticSilentRenew is configured, setting up silent renew"); this.startSilentRenew(); } if (this.settings.monitorSession) { Log.debug("UserManager.ctor: monitorSession is configured, setting up session monitor"); this._sessionMonitor = new SessionMonitorCtor(this); } this._tokenRevocationClient = new TokenRevocationClientCtor(this._settings); this._tokenClient = new TokenClientCtor(this._settings); this._joseUtil = joseUtil; } get _redirectNavigator() { return this.settings.redirectNavigator; } get _popupNavigator() { return this.settings.popupNavigator; } get _iframeNavigator() { return this.settings.iframeNavigator; } get _userStore() { return this.settings.userStore; } get events() { return this._events; } getUser() { return this._loadUser().then(user => { if (user) { Log.info("UserManager.getUser: user loaded"); this._events.load(user, false); return user; } else { Log.info("UserManager.getUser: user not found in storage"); return null; } }); } removeUser() { return this.storeUser(null).then(() => { Log.info("UserManager.removeUser: user removed from storage"); this._events.unload(); }); } signinRedirect(args = {}) { args = Object.assign({}, args); args.request_type = "si:r"; let navParams = { useReplaceToNavigate : args.useReplaceToNavigate }; return this._signinStart(args, this._redirectNavigator, navParams).then(()=>{ Log.info("UserManager.signinRedirect: successful"); }); } signinRedirectCallback(url) { return this._signinEnd(url || this._redirectNavigator.url).then(user => { if (user.profile && user.profile.sub) { Log.info("UserManager.signinRedirectCallback: successful, signed in sub: ", user.profile.sub); } else { Log.info("UserManager.signinRedirectCallback: no sub"); } return user; }); } signinPopup(args = {}) { args = Object.assign({}, args); args.request_type = "si:p"; let url = args.redirect_uri || this.settings.popup_redirect_uri || this.settings.redirect_uri; if (!url) { Log.error("UserManager.signinPopup: No popup_redirect_uri or redirect_uri configured"); return Promise.reject(new Error("No popup_redirect_uri or redirect_uri configured")); } args.redirect_uri = url; args.display = "popup"; return this._signin(args, this._popupNavigator, { startUrl: url, popupWindowFeatures: args.popupWindowFeatures || this.settings.popupWindowFeatures, popupWindowTarget: args.popupWindowTarget || this.settings.popupWindowTarget }).then(user => { if (user) { if (user.profile && user.profile.sub) { Log.info("UserManager.signinPopup: signinPopup successful, signed in sub: ", user.profile.sub); } else { Log.info("UserManager.signinPopup: no sub"); } } return user; }); } signinPopupCallback(url) { return this._signinCallback(url, this._popupNavigator).then(user => { if (user) { if (user.profile && user.profile.sub) { Log.info("UserManager.signinPopupCallback: successful, signed in sub: ", user.profile.sub); } else { Log.info("UserManager.signinPopupCallback: no sub"); } } return user; }).catch(err=>{ Log.error("UserManager.signinPopupCallback error: " + err && err.message); }); } signinSilent(args = {}) { args = Object.assign({}, args); // first determine if we have a refresh token, or need to use iframe return this._loadUser().then(user => { if (user && user.refresh_token) { args.refresh_token = user.refresh_token; return this._useRefreshToken(args); } else { args.request_type = "si:s"; args.id_token_hint = args.id_token_hint || (this.settings.includeIdTokenInSilentRenew && user && user.id_token); if (user && this._settings.validateSubOnSilentRenew) { Log.debug("UserManager.signinSilent, subject prior to silent renew: ", user.profile.sub); args.current_sub = user.profile.sub; } return this._signinSilentIframe(args); } }); } _useRefreshToken(args = {}) { return this._tokenClient.exchangeRefreshToken(args).then(result => { if (!result) { Log.error("UserManager._useRefreshToken: No response returned from token endpoint"); return Promise.reject("No response returned from token endpoint"); } if (!result.access_token) { Log.error("UserManager._useRefreshToken: No access token returned from token endpoint"); return Promise.reject("No access token returned from token endpoint"); } return this._loadUser().then(user => { if (user) { let idTokenValidation = Promise.resolve(); if (result.id_token) { idTokenValidation = this._validateIdTokenFromTokenRefreshToken(user.profile, result.id_token); } return idTokenValidation.then(() => { Log.debug("UserManager._useRefreshToken: refresh token response success"); user.id_token = result.id_token || user.id_token; user.access_token = result.access_token; user.refresh_token = result.refresh_token || user.refresh_token; user.expires_in = result.expires_in; return this.storeUser(user).then(()=>{ this._events.load(user); return user; }); }); } else { return null; } }); }); } _validateIdTokenFromTokenRefreshToken(profile, id_token) { return this._metadataService.getIssuer().then(issuer => { return this.settings.getEpochTime().then(now => { return this._joseUtil.validateJwtAttributes(id_token, issuer, this._settings.client_id, this._settings.clockSkew, now).then(payload => { if (!payload) { Log.error("UserManager._validateIdTokenFromTokenRefreshToken: Failed to validate id_token"); return Promise.reject(new Error("Failed to validate id_token")); } if (payload.sub !== profile.sub) { Log.error("UserManager._validateIdTokenFromTokenRefreshToken: sub in id_token does not match current sub"); return Promise.reject(new Error("sub in id_token does not match current sub")); } if (payload.auth_time && payload.auth_time !== profile.auth_time) { Log.error("UserManager._validateIdTokenFromTokenRefreshToken: auth_time in id_token does not match original auth_time"); return Promise.reject(new Error("auth_time in id_token does not match original auth_time")); } if (payload.azp && payload.azp !== profile.azp) { Log.error("UserManager._validateIdTokenFromTokenRefreshToken: azp in id_token does not match original azp"); return Promise.reject(new Error("azp in id_token does not match original azp")); } if (!payload.azp && profile.azp) { Log.error("UserManager._validateIdTokenFromTokenRefreshToken: azp not in id_token, but present in original id_token"); return Promise.reject(new Error("azp not in id_token, but present in original id_token")); } }); }); }); } _signinSilentIframe(args = {}) { let url = args.redirect_uri || this.settings.silent_redirect_uri || this.settings.redirect_uri; if (!url) { Log.error("UserManager.signinSilent: No silent_redirect_uri configured"); return Promise.reject(new Error("No silent_redirect_uri configured")); } args.redirect_uri = url; args.prompt = args.prompt || "none"; return this._signin(args, this._iframeNavigator, { startUrl: url, silentRequestTimeout: args.silentRequestTimeout || this.settings.silentRequestTimeout }).then(user => { if (user) { if (user.profile && user.profile.sub) { Log.info("UserManager.signinSilent: successful, signed in sub: ", user.profile.sub); } else { Log.info("UserManager.signinSilent: no sub"); } } return user; }); } signinSilentCallback(url) { return this._signinCallback(url, this._iframeNavigator).then(user => { if (user) { if (user.profile && user.profile.sub) { Log.info("UserManager.signinSilentCallback: successful, signed in sub: ", user.profile.sub); } else { Log.info("UserManager.signinSilentCallback: no sub"); } } return user; }); } signinCallback(url) { return this.readSigninResponseState(url).then(({state, response}) => { if (state.request_type === "si:r") { return this.signinRedirectCallback(url); } if (state.request_type === "si:p") { return this.signinPopupCallback(url); } if (state.request_type === "si:s") { return this.signinSilentCallback(url); } return Promise.reject(new Error("invalid response_type in state")); }); } signoutCallback(url, keepOpen) { return this.readSignoutResponseState(url).then(({state, response}) => { if (state) { if (state.request_type === "so:r") { return this.signoutRedirectCallback(url); } if (state.request_type === "so:p") { return this.signoutPopupCallback(url, keepOpen); } return Promise.reject(new Error("invalid response_type in state")); } return response; }); } querySessionStatus(args = {}) { args = Object.assign({}, args); args.request_type = "si:s"; // this acts like a signin silent let url = args.redirect_uri || this.settings.silent_redirect_uri || this.settings.redirect_uri; if (!url) { Log.error("UserManager.querySessionStatus: No silent_redirect_uri configured"); return Promise.reject(new Error("No silent_redirect_uri configured")); } args.redirect_uri = url; args.prompt = "none"; args.response_type = args.response_type || this.settings.query_status_response_type; args.scope = args.scope || "openid"; args.skipUserInfo = true; return this._signinStart(args, this._iframeNavigator, { startUrl: url, silentRequestTimeout: args.silentRequestTimeout || this.settings.silentRequestTimeout }).then(navResponse => { return this.processSigninResponse(navResponse.url).then(signinResponse => { Log.debug("UserManager.querySessionStatus: got signin response"); if (signinResponse.session_state && signinResponse.profile.sub) { Log.info("UserManager.querySessionStatus: querySessionStatus success for sub: ", signinResponse.profile.sub); return { session_state: signinResponse.session_state, sub: signinResponse.profile.sub, sid: signinResponse.profile.sid }; } else { Log.info("querySessionStatus successful, user not authenticated"); } }) .catch(err => { if (err.session_state && this.settings.monitorAnonymousSession) { if (err.message == "login_required" || err.message == "consent_required" || err.message == "interaction_required" || err.message == "account_selection_required" ) { Log.info("UserManager.querySessionStatus: querySessionStatus success for anonymous user"); return { session_state: err.session_state }; } } throw err; }); }); } _signin(args, navigator, navigatorParams = {}) { return this._signinStart(args, navigator, navigatorParams).then(navResponse => { return this._signinEnd(navResponse.url, args); }); } _signinStart(args, navigator, navigatorParams = {}) { return navigator.prepare(navigatorParams).then(handle => { Log.debug("UserManager._signinStart: got navigator window handle"); return this.createSigninRequest(args).then(signinRequest => { Log.debug("UserManager._signinStart: got signin request"); navigatorParams.url = signinRequest.url; navigatorParams.id = signinRequest.state.id; return handle.navigate(navigatorParams); }).catch(err => { if (handle.close) { Log.debug("UserManager._signinStart: Error after preparing navigator, closing navigator window"); handle.close(); } throw err; }); }); } _signinEnd(url, args = {}) { return this.processSigninResponse(url).then(signinResponse => { Log.debug("UserManager._signinEnd: got signin response"); let user = new User(signinResponse); if (args.current_sub) { if (args.current_sub !== user.profile.sub) { Log.debug("UserManager._signinEnd: current user does not match user returned from signin. sub from signin: ", user.profile.sub); return Promise.reject(new Error("login_required")); } else { Log.debug("UserManager._signinEnd: current user matches user returned from signin"); } } return this.storeUser(user).then(() => { Log.debug("UserManager._signinEnd: user stored"); this._events.load(user); return user; }); }); } _signinCallback(url, navigator) { Log.debug("UserManager._signinCallback"); let useQuery = this._settings.response_mode === "query" || (!this._settings.response_mode && SigninRequest.isCode(this._settings.response_type)); let delimiter = useQuery ? "?" : "#"; return navigator.callback(url, undefined, delimiter); } signoutRedirect(args = {}) { args = Object.assign({}, args); args.request_type = "so:r"; let postLogoutRedirectUri = args.post_logout_redirect_uri || this.settings.post_logout_redirect_uri; if (postLogoutRedirectUri){ args.post_logout_redirect_uri = postLogoutRedirectUri; } let navParams = { useReplaceToNavigate : args.useReplaceToNavigate }; return this._signoutStart(args, this._redirectNavigator, navParams).then(()=>{ Log.info("UserManager.signoutRedirect: successful"); }); } signoutRedirectCallback(url) { return this._signoutEnd(url || this._redirectNavigator.url).then(response=>{ Log.info("UserManager.signoutRedirectCallback: successful"); return response; }); } signoutPopup(args = {}) { args = Object.assign({}, args); args.request_type = "so:p"; let url = args.post_logout_redirect_uri || this.settings.popup_post_logout_redirect_uri || this.settings.post_logout_redirect_uri; args.post_logout_redirect_uri = url; args.display = "popup"; if (args.post_logout_redirect_uri){ // we're putting a dummy entry in here because we // need a unique id from the state for notification // to the parent window, which is necessary if we // plan to return back to the client after signout // and so we can close the popup after signout args.state = args.state || {}; } return this._signout(args, this._popupNavigator, { startUrl: url, popupWindowFeatures: args.popupWindowFeatures || this.settings.popupWindowFeatures, popupWindowTarget: args.popupWindowTarget || this.settings.popupWindowTarget }).then(() => { Log.info("UserManager.signoutPopup: successful"); }); } signoutPopupCallback(url, keepOpen) { if (typeof(keepOpen) === 'undefined' && typeof(url) === 'boolean') { keepOpen = url; url = null; } let delimiter = '?'; return this._popupNavigator.callback(url, keepOpen, delimiter).then(() => { Log.info("UserManager.signoutPopupCallback: successful"); }); } _signout(args, navigator, navigatorParams = {}) { return this._signoutStart(args, navigator, navigatorParams).then(navResponse => { return this._signoutEnd(navResponse.url); }); } _signoutStart(args = {}, navigator, navigatorParams = {}) { return navigator.prepare(navigatorParams).then(handle => { Log.debug("UserManager._signoutStart: got navigator window handle"); return this._loadUser().then(user => { Log.debug("UserManager._signoutStart: loaded current user from storage"); var revokePromise = this._settings.revokeAccessTokenOnSignout ? this._revokeInternal(user) : Promise.resolve(); return revokePromise.then(() => { var id_token = args.id_token_hint || user && user.id_token; if (id_token) { Log.debug("UserManager._signoutStart: Setting id_token into signout request"); args.id_token_hint = id_token; } return this.removeUser().then(() => { Log.debug("UserManager._signoutStart: user removed, creating signout request"); return this.createSignoutRequest(args).then(signoutRequest => { Log.debug("UserManager._signoutStart: got signout request"); navigatorParams.url = signoutRequest.url; if (signoutRequest.state) { navigatorParams.id = signoutRequest.state.id; } return handle.navigate(navigatorParams); }); }); }); }).catch(err => { if (handle.close) { Log.debug("UserManager._signoutStart: Error after preparing navigator, closing navigator window"); handle.close(); } throw err; }); }); } _signoutEnd(url) { return this.processSignoutResponse(url).then(signoutResponse => { Log.debug("UserManager._signoutEnd: got signout response"); return signoutResponse; }); } revokeAccessToken() { return this._loadUser().then(user => { return this._revokeInternal(user, true).then(success => { if (success) { Log.debug("UserManager.revokeAccessToken: removing token properties from user and re-storing"); user.access_token = null; user.refresh_token = null; user.expires_at = null; user.token_type = null; return this.storeUser(user).then(() => { Log.debug("UserManager.revokeAccessToken: user stored"); this._events.load(user); }); } }); }).then(()=>{ Log.info("UserManager.revokeAccessToken: access token revoked successfully"); }); } _revokeInternal(user, required) { if (user) { var access_token = user.access_token; var refresh_token = user.refresh_token; return this._revokeAccessTokenInternal(access_token, required) .then(atSuccess => { return this._revokeRefreshTokenInternal(refresh_token, required) .then(rtSuccess => { if (!atSuccess && !rtSuccess) { Log.debug("UserManager.revokeAccessToken: no need to revoke due to no token(s), or JWT format"); } return atSuccess || rtSuccess; }); }); } return Promise.resolve(false); } _revokeAccessTokenInternal(access_token, required) { // check for JWT vs. reference token if (!access_token || access_token.indexOf('.') >= 0) { return Promise.resolve(false); } return this._tokenRevocationClient.revoke(access_token, required).then(() => true); } _revokeRefreshTokenInternal(refresh_token, required) { if (!refresh_token) { return Promise.resolve(false); } return this._tokenRevocationClient.revoke(refresh_token, required, "refresh_token").then(() => true); } startSilentRenew() { this._silentRenewService.start(); } stopSilentRenew() { this._silentRenewService.stop(); } get _userStoreKey() { return `user:${this.settings.authority}:${this.settings.client_id}`; } _loadUser() { return this._userStore.get(this._userStoreKey).then(storageString => { if (storageString) { Log.debug("UserManager._loadUser: user storageString loaded"); return User.fromStorageString(storageString); } Log.debug("UserManager._loadUser: no user storageString"); return null; }); } storeUser(user) { if (user) { Log.debug("UserManager.storeUser: storing user"); var storageString = user.toStorageString(); return this._userStore.set(this._userStoreKey, storageString); } else { Log.debug("storeUser.storeUser: removing user"); return this._userStore.remove(this._userStoreKey); } } }