UNPKG

auth0-sso-login

Version:

A Library to simplify the auth0 sso login process

544 lines (458 loc) 21.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _jsonwebtoken = _interopRequireDefault(require("jsonwebtoken")); var _windowInteraction = _interopRequireDefault(require("./window-interaction")); var _tokenExpiryManager = _interopRequireDefault(require("./token-expiry-manager")); var _redirectHandler = _interopRequireDefault(require("./redirectHandler")); var _logger = _interopRequireDefault(require("./logger")); var _auth0ClientProvider = _interopRequireDefault(require("./auth0ClientProvider")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } // authentication class var auth = /*#__PURE__*/function () { /** * @constructor constructs the object with a given configuration * @param {Object} config * @param {string} config.clientId the auth0 client ID to be used - see https://auth0.com/docs/api-auth/tutorials/client-credentials * @param {string} config.domain the auth0 domain to login - see https://auth0.com/docs/api-auth/tutorials/client-credentials * @param {string} config.audience the auth0 audience - see https://auth0.com/docs/api-auth/tutorials/client-credentials * @param {string} [config.timeout=5000] timeout in milliseconds attempting to call auth0 - this can fail when the auth0 domain is blocked * @param {string} [config.logoutRedirectUri=${window.location.origin}/#/logout] the logout URL, which should be accessible by a non-authenticated user, default is `window.location.href` * @param {string} [config.applicationRoot=/] the application root, by default the redirect from universal lock will redirect here before replacing history with the specified redirect. * @param {string} [config.explicitConnection] specify an explicit connection to use, which allows bypassing the lock widget * @param {Object} hooks hooks to get callback calls into the login/logout workflow * @param {Function} config.logout (redirectUri) before the redirect to the redirectUri happens (with fallback to logoutRedirectUri and then to window.location.href) * @param {Function} config.profileRefreshed (profile) the profile was retrieved, this is an option to store the profile, or update the user interface * @param {Function} config.tokenRefreshed the auth token was retrieved, this is an option to store the token for later use * @param {Function} config.removeLogin called before logout or when there's a problem with the current user, for example an invalid token * @param {Function} config.log (messageObject) allows to override log messages; defaults to log to the console */ function auth(config) { _classCallCheck(this, auth); this.config = config || {}; this.authResult = null; var logger = new _logger["default"](config); this.logger = logger; this.tokenExpiryManager = new _tokenExpiryManager["default"](); this.redirectHandler = new _redirectHandler["default"](logger); this.renewAuthSequencePromise = Promise.resolve(); this.auth0ClientProvider = new _auth0ClientProvider["default"](config); } /** * @description update the detailed profile with a call to the Auth0 Management API * @return {Promise<any>} resolved promise with user profile; rejected promise with error */ _createClass(auth, [{ key: "refreshProfile", value: function refreshProfile() { var _this = this; // If there is no hook defined preemptively looking up the profile doesn't do any good. if (!this.config.hooks || !this.config.hooks.profileRefreshed) { return Promise.resolve(); } return this.getProfile().then(function (profile) { _this.config.hooks.profileRefreshed(profile); }, function (error) { _this.logger.log({ title: 'Error while retrieving user information after successful authentication', errorCode: 'ProfileError', error: error }); }); } /** * @description Get the latest available profile * @return {null|Object} profile if the user was already logged in; null otherwise */ }, { key: "getProfile", value: function getProfile() { var _this2 = this; return Promise.resolve().then(function () { var idToken = _this2.getIdToken(); var jwt = _jsonwebtoken["default"].decode(idToken); var auth0AccessToken = _this2.authResult && _this2.authResult.idToken; if (!jwt || !jwt.sub || !auth0AccessToken) { throw Error('Current idToken or auth0AccessToken is not available.'); } return new Promise(function (resolve, reject) { _this2.auth0ClientProvider.getManagementClient(auth0AccessToken).getUser(jwt.sub, function (error, profile) { return error ? reject({ title: 'Failed to get profile', error: error }) : resolve(profile); }); }); }); } /** * @description Get the latest available idToken * @return {null|String} idToken if the user was already logged in; null otherwise */ }, { key: "getIdToken", value: function getIdToken() { var idToken = this.authResult && this.authResult.accessToken; try { var validToken = idToken && _jsonwebtoken["default"].decode(idToken).exp > Math.floor(Date.now() / 1000) ? idToken : null; if (validToken) { this.tokenExpiryManager.createSession(); } return validToken; } catch (e) { this.logger.log({ title: 'JWTTokenException', errorCode: 'JWTTokenException', invalidToken: idToken, error: e }); return null; } } /** * @description Calls a hook once the token got refreshed * @param authResult authorization result returned by auth0 * @return {Promise<>} */ }, { key: "tokenRefreshed", value: function tokenRefreshed(authResult) { var _this3 = this; this.authResult = authResult; this.tokenExpiryManager.scheduleTokenRefresh(authResult, function () { return _this3.ensureLoggedIn({ enabledHostedLogin: true, forceTokenRefresh: true }); }); if (this.config.hooks && this.config.hooks.tokenRefreshed) { return this.config.hooks.tokenRefreshed(); } return Promise.resolve(); } /** * Calls a hook once the login should be removed * @return {Promise<>} */ }, { key: "removeLogin", value: function removeLogin() { this.tokenExpiryManager.cancelTokenRefresh(); this.authResult = null; if (this.config.hooks && this.config.hooks.removeLogin) { return this.config.hooks.removeLogin(); } return Promise.resolve(); } /** * @description Calls a hook to removeLogin and logout the user, and then interacts with Auth0 to * actually log the user out. * @param redirectUriOverride Override redirect location after logout. */ }, { key: "logout", value: function logout(redirectUriOverride) { this.tokenExpiryManager.cancelTokenRefresh(); this.authResult = null; if (this.config) { if (this.config.hooks && this.config.hooks.removeLogin) { this.config.hooks.removeLogin(); } if (this.config.hooks && this.config.hooks.logout) { this.config.hooks.logout(); } var redirectUri = encodeURIComponent(redirectUriOverride || this.config.logoutRedirectUri || window.location.href); _windowInteraction["default"].updateWindow("https://".concat(this.config.domain, "/v2/logout?returnTo=").concat(redirectUri, "&client_id=").concat(this.config.clientId)); } } /** * @description Ensure user is logged in: * 1. Check if there is an existing, valid token. * 2. Try logging in using an existing SSO session. * 3. If universal login is not explicitly disabled, try logging in via the hosted login * * @param {Object} configuration object * @param {Boolean} configuration.enabledHostedLogin whether the universal login should open when SSO * session is invalid; default = true * @param {Boolean} configuration.forceTokenRefresh if token should be refreshed even if it may * be still valid; default = false * @param {String} configuration.redirectUri Override redirect location after universal login. * @param {String} configuration.explicitConnection Override specified connection for universal login. * @param {Boolean} configuration.requireValidSession Require that a valid token was retrieved once before, if not returns immediately, no token will be created. * Token validation will still be required. * @return {Promise<Object>} optional redirectUri on successful login if a redirect needs to still happen; otherwise rejected promise with error */ }, { key: "ensureLoggedIn", value: function () { var _ensureLoggedIn = _asyncToGenerator( /*#__PURE__*/_regenerator["default"].mark(function _callee() { var _this4 = this; var configuration, latestAuthResult, redirectFromAuth0Result, errorCode, updatedError, containsToken, redirectUri, authPromise, _args = arguments; return _regenerator["default"].wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: configuration = _args.length > 0 && _args[0] !== undefined ? _args[0] : { enabledHostedLogin: true, forceTokenRefresh: false, requireValidSession: false }; // if there is still a valid token, there is no need to initiate the login process latestAuthResult = this.getIdToken(); if (!(!configuration.forceTokenRefresh && latestAuthResult && this.tokenExpiryManager.getRemainingMillisToTokenExpiry() > 0)) { _context.next = 4; break; } return _context.abrupt("return", Promise.resolve()); case 4: if (!(configuration.requireValidSession && !this.tokenExpiryManager.authorizationSessionExists())) { _context.next = 6; break; } return _context.abrupt("return", Promise.resolve()); case 6: _context.prev = 6; _context.next = 9; return new Promise(function (resolve, reject) { return _this4.auth0ClientProvider.getClient().parseHash({}, function (error, authResult) { return error ? reject(error) : resolve(authResult); }); }); case 9: redirectFromAuth0Result = _context.sent; _context.next = 20; break; case 12: _context.prev = 12; _context.t0 = _context["catch"](6); errorCode = _context.t0.error; if (_context.t0.error === 'access_denied' && _context.t0.errorDescription === 'Please verify your email before logging in.') { errorCode = 'UnverifiedEmail'; } this.logger.log({ title: 'Login error found', level: 'WARN', url: window.location.href, error: _context.t0, errorCode: errorCode, description: _context.t0.errorDescription }); // In the case of an invalid token, skip throwing the error here and fallback to fetching a valid token directly from Auth0. This can happen if state or nonce was attempted to be hijacked. // Instead of telling the user or forcing them to login again manually, defeat the CSRF or replay-attack by automatically authing with Auth0 directly. This will happen in `renewAuth`. if (!(_context.t0.error !== 'invalid_token')) { _context.next = 20; break; } updatedError = { details: _context.t0.errorDescription, errorCode: errorCode }; throw updatedError; case 20: containsToken = redirectFromAuth0Result && redirectFromAuth0Result.idToken && redirectFromAuth0Result.accessToken; if (!containsToken) { _context.next = 35; break; } this.authResult = redirectFromAuth0Result; _context.prev = 23; _context.next = 26; return this.refreshProfile(); case 26: _context.next = 28; return this.tokenRefreshed(this.authResult); case 28: _context.next = 33; break; case 30: _context.prev = 30; _context.t1 = _context["catch"](23); this.logger.log({ title: 'Failed to fire "Token Refreshed" event', errorCode: 'TokenRefreshFailed', error: _context.t1 }); case 33: redirectUri = this.redirectHandler.attemptRedirect(); return _context.abrupt("return", { redirectUri: redirectUri }); case 35: authPromise = this.renewAuthSequencePromise.then(function () { return _this4.renewAuth(); })["catch"](function (e) { // if universal login is not enabled, error out if (!configuration.enabledHostedLogin) { throw e; } _this4.logger.log({ title: 'Renew authorization did not succeed, falling back to Auth0 universal login.', errorCode: 'RenewAuthorizationFailure', error: e }); return _this4.universalAuth(configuration.redirectUri, configuration.explicitConnection); }).then(function () { _this4.clearOldNonces(); })["catch"](function (err) { _this4.removeLogin(); throw err; }); this.renewAuthSequencePromise = authPromise["catch"](function () { /* ignore since renewAuthSequcne may never be a rejected promise to have successful continuations */ }); return _context.abrupt("return", authPromise); case 38: case "end": return _context.stop(); } } }, _callee, this, [[6, 12], [23, 30]]); })); function ensureLoggedIn() { return _ensureLoggedIn.apply(this, arguments); } return ensureLoggedIn; }() /** unfortunate, but localStorage can fill up if this isn't called. * should only be called after successful authentication has completed to avoid * removing in process nonces * https://github.com/auth0/auth0.js/issues/402 * @description Cleanup old auth0 localstorage */ }, { key: "clearOldNonces", value: function clearOldNonces() { try { Object.keys(localStorage).forEach(function (key) { if (!key.startsWith('com.auth0.auth')) { return; } localStorage.removeItem(key); }); } catch (erorr) { /* */ } } /** * @description uses the hosted login page to login * @param redirectUri url to return to otherwise `window.location.href` will be used. * @param explicitConnection connection to force using for the universal login, will bypass showing auth0 lock widget. * @return {Promise<any>} */ }, { key: "universalAuth", value: function universalAuth(redirectUri, explicitConnection) { var _this5 = this; this.redirectHandler.setRedirect(redirectUri || window.location.href); var redirectUriRoot = window.location.origin || "".concat(window.location.protocol, "//").concat(window.location.hostname).concat(window.location.port ? ":".concat(window.location.port) : ''); var options = { redirectUri: "".concat(redirectUriRoot).concat(this.config.applicationRoot || ''), audience: this.config.audience, responseType: 'id_token token', connection: explicitConnection || this.config.explicitConnection, prompt: explicitConnection || this.config.explicitConnection ? 'select_account' : undefined }; return new Promise(function (resolve, reject) { _this5.logger.log({ title: 'Redirecting to login page and waiting for result.' }); _this5.auth0ClientProvider.getClient().authorize(options, function (error, authResult) { if (error) { _this5.logger.log({ title: 'Redirect to login page failed.', errorCode: 'RedirectFailed', error: error }); return reject(error); } return resolve(authResult); }); }); } /** * @description renews the authentication * @param {Number} retries current retry attempt number * @return {Promise<any>} */ }, { key: "renewAuth", value: function renewAuth() { var _this6 = this; var retries = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; var redirectUriRoot = window.location.origin || "".concat(window.location.protocol, "//").concat(window.location.hostname).concat(window.location.port ? ":".concat(window.location.port) : ''); var renewOptions = { redirectUri: "".concat(redirectUriRoot).concat(this.config.applicationRoot || ''), audience: this.config.audience, responseType: 'id_token token', timeout: this.config.timeout || 5000 }; return new Promise(function (resolve, reject) { _this6.auth0ClientProvider.getClient().checkSession(renewOptions, function (err, authResult) { if (err) { return reject(err); } if (authResult && authResult.accessToken && authResult.idToken) { _this6.authResult = authResult; return _this6.refreshProfile().then(function () { return _this6.tokenRefreshed(authResult); })["catch"](function (error) { _this6.logger.log({ title: 'Failed to fire "Token Refreshed" event', errorCode: 'TokenRefreshFailed', error: error }); }).then(function () { resolve(); }); } return reject({ error: 'no_token_available', errorDescription: 'Failed to get valid token.', authResultError: authResult ? authResult.error : undefined }); }); })["catch"](function (error) { _this6.logger.log({ title: 'Failed to update ID token on retry', errorCode: 'IdTokenUpdateFailed', retry: retries, error: error }); var fatalErrors = { consent_required: true, login_required: true }; if (fatalErrors[error.error]) { throw error; } if (retries < 4 && error.authResultError === undefined) { return new Promise(function (resolve) { return setTimeout(function () { return resolve(); }, 1000); }).then(function () { return _this6.renewAuth(retries + 1); }); } throw error; }); } }]); return auth; }(); exports["default"] = auth;