auth0-sso-login
Version:
A Library to simplify the auth0 sso login process
544 lines (458 loc) • 21.7 kB
JavaScript
"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;