| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666 |
- /* eslint-disable complexity, max-statements */
- var http = require('./http');
- var util = require('./util');
- var oauthUtil = require('./oauthUtil');
- var Q = require('q');
- var sdkCrypto = require('./crypto');
- var AuthSdkError = require('./errors/AuthSdkError');
- var OAuthError = require('./errors/OAuthError');
- var config = require('./config');
- var cookies = require('./cookies');
-
- function decodeToken(token) {
- var jwt = token.split('.');
- var decodedToken;
-
- try {
- decodedToken = {
- header: JSON.parse(util.base64UrlToString(jwt[0])),
- payload: JSON.parse(util.base64UrlToString(jwt[1])),
- signature: jwt[2]
- };
- } catch(e) {
- throw new AuthSdkError('Malformed token');
- }
-
- return decodedToken;
- }
-
- function verifyIdToken(sdk, idToken, options) {
- options = options || {};
-
- if (!sdk.features.isTokenVerifySupported()) {
- return Q.reject(new AuthSdkError('This browser doesn\'t support crypto.subtle'));
- }
-
- function isExpired(jwtExp) {
- var expirationTime;
- if (options.expirationTime || options.expirationTime === 0) {
- expirationTime = options.expirationTime;
- } else {
- expirationTime = Math.floor(Date.now()/1000);
- }
- if (jwtExp &&
- jwtExp > expirationTime) {
- return true;
- }
- }
-
- function hasAudience(jwtAudience) {
- if (!options.audience) {
- return true;
- }
- var audiences = Array.isArray(options.audience) ? options.audience : [options.audience];
- var jwtAudiences = Array.isArray(jwtAudience) ? jwtAudience : [jwtAudience];
- var ai = audiences.length;
- while (ai--) {
- var aud = audiences[ai];
- if (jwtAudiences.indexOf(aud) !== -1) {
- return true;
- }
- }
- }
-
- return oauthUtil.getWellKnown(sdk)
- .then(function(res) {
- return http.get(sdk, res['jwks_uri']);
- })
- .then(function(res) {
- var key = res.keys[0];
- return sdkCrypto.verifyToken(idToken, key);
- })
- .then(function(res) {
- if (!res) {
- return false;
- }
- var jwt = sdk.token.decode(idToken);
-
- if (isExpired(jwt.payload.exp)) {
- return false;
- }
-
- if (!hasAudience(jwt.payload.aud)) {
- return false;
- }
-
- if (options.issuer &&
- options.issuer !== jwt.payload.iss) {
- return false;
- }
-
- return true;
- });
- }
-
- function verifyToken(sdk, token, nonce, ignoreSignature) {
- return new Q()
- .then(function() {
- if (!token || !token.idToken) {
- throw new AuthSdkError('Only idTokens may be verified');
- }
-
- var jwt = decodeToken(token.idToken);
-
- // Standard claim validation
- oauthUtil.validateClaims(sdk, jwt.payload, token.clientId, token.issuer, nonce);
-
- // If the browser doesn't support native crypto or we choose not
- // to verify the signature, bail early
- if (ignoreSignature || !sdk.features.isTokenVerifySupported()) {
- return token;
- }
-
- return oauthUtil.getKey(sdk, token.issuer, jwt.header.kid)
- .then(function(key) {
- return sdkCrypto.verifyToken(token.idToken, key);
- })
- .then(function(valid) {
- if (!valid) {
- throw new AuthSdkError('The token signature is not valid');
- }
- return token;
- });
- });
- }
-
- function refreshIdToken(sdk, options) {
- options = options || {};
- options.display = null;
- options.prompt = 'none';
- return getToken(sdk, options);
- }
-
- function addPostMessageListener(sdk, timeout, state) {
- var deferred = Q.defer();
-
- function responseHandler(e) {
- if (!e.data ||
- e.origin !== sdk.options.url ||
- (e.data && util.isString(state) && e.data.state !== state)) {
- return;
- }
- deferred.resolve(e.data);
- }
-
- oauthUtil.addListener(window, 'message', responseHandler);
-
- return deferred.promise.timeout(timeout || 120000, new AuthSdkError('OAuth flow timed out'))
- .fin(function() {
- oauthUtil.removeListener(window, 'message', responseHandler);
- });
- }
-
- function addFragmentListener(sdk, windowEl, timeout) {
- var deferred = Q.defer();
-
- function hashChangeHandler() {
- /*
- We are only able to access window.location.hash on a window
- that has the same domain. A try/catch is necessary because
- there's no other way to determine that the popup is in
- another domain. When we try to access a window on another
- domain, an error is thrown.
- */
- try {
- if (windowEl &&
- windowEl.location &&
- windowEl.location.hash) {
- deferred.resolve(oauthUtil.hashToObject(windowEl.location.hash));
- } else if (windowEl && !windowEl.closed) {
- setTimeout(hashChangeHandler, 500);
- }
- } catch (err) {
- setTimeout(hashChangeHandler, 500);
- }
- }
-
- hashChangeHandler();
-
- return deferred.promise.timeout(timeout || 120000, new AuthSdkError('OAuth flow timed out'));
- }
-
- function handleOAuthResponse(sdk, oauthParams, res, urls) {
- urls = urls || {};
-
- var tokenTypes = oauthParams.responseType;
- var scopes = util.clone(oauthParams.scopes);
- var clientId = oauthParams.clientId || sdk.options.clientId;
-
- return new Q()
- .then(function() {
- if (res['error'] || res['error_description']) {
- throw new OAuthError(res['error'], res['error_description']);
- }
-
- if (res.state !== oauthParams.state) {
- throw new AuthSdkError('OAuth flow response state doesn\'t match request state');
- }
-
- var tokenDict = {};
-
- if (res['access_token']) {
- tokenDict['token'] = {
- accessToken: res['access_token'],
- expiresAt: Number(res['expires_in']) + Math.floor(Date.now()/1000),
- tokenType: res['token_type'],
- scopes: scopes,
- authorizeUrl: urls.authorizeUrl,
- userinfoUrl: urls.userinfoUrl
- };
- }
-
- if (res['code']) {
- tokenDict['code'] = {
- authorizationCode: res['code']
- };
- }
-
- if (res['id_token']) {
- var jwt = sdk.token.decode(res['id_token']);
-
- var idToken = {
- idToken: res['id_token'],
- claims: jwt.payload,
- expiresAt: jwt.payload.exp,
- scopes: scopes,
- authorizeUrl: urls.authorizeUrl,
- issuer: urls.issuer,
- clientId: clientId
- };
-
- return verifyToken(sdk, idToken, oauthParams.nonce, true)
- .then(function() {
- tokenDict['id_token'] = idToken;
- return tokenDict;
- });
- }
-
- return tokenDict;
- })
- .then(function(tokenDict) {
- if (!Array.isArray(tokenTypes)) {
- return tokenDict[tokenTypes];
- }
-
- if (!tokenDict['token'] && !tokenDict['id_token']) {
- throw new AuthSdkError('Unable to parse OAuth flow response');
- }
-
- // Create token array in the order of the responseType array
- return tokenTypes.map(function(item) {
- return tokenDict[item];
- });
- });
- }
-
- function getDefaultOAuthParams(sdk, oauthOptions) {
- oauthOptions = util.clone(oauthOptions) || {};
-
- if (oauthOptions.scope) {
- util.deprecate('The param "scope" is equivalent to "scopes". Use "scopes" instead.');
- oauthOptions.scopes = oauthOptions.scope;
- delete oauthOptions.scope;
- }
-
- var defaults = {
- clientId: sdk.options.clientId,
- redirectUri: sdk.options.redirectUri || window.location.href,
- responseType: 'id_token',
- responseMode: 'okta_post_message',
- state: util.genRandomString(64),
- nonce: util.genRandomString(64),
- scopes: ['openid', 'email']
- };
- util.extend(defaults, oauthOptions);
- return defaults;
- }
-
- function convertOAuthParamsToQueryParams(oauthParams) {
- // Quick validation
- if (!oauthParams.clientId) {
- throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to get a token');
- }
-
- if (util.isString(oauthParams.responseType) && oauthParams.responseType.indexOf(' ') !== -1) {
- throw new AuthSdkError('Multiple OAuth responseTypes must be defined as an array');
- }
-
- // Convert our params to their actual OAuth equivalents
- var oauthQueryParams = util.removeNils({
- 'client_id': oauthParams.clientId,
- 'redirect_uri': oauthParams.redirectUri,
- 'response_type': oauthParams.responseType,
- 'response_mode': oauthParams.responseMode,
- 'state': oauthParams.state,
- 'nonce': oauthParams.nonce,
- 'prompt': oauthParams.prompt,
- 'display': oauthParams.display,
- 'sessionToken': oauthParams.sessionToken,
- 'idp': oauthParams.idp,
- 'max_age': oauthParams.maxAge
- });
-
- if (Array.isArray(oauthQueryParams['response_type'])) {
- oauthQueryParams['response_type'] = oauthQueryParams['response_type'].join(' ');
- }
-
- if (oauthParams.responseType.indexOf('id_token') !== -1 &&
- oauthParams.scopes.indexOf('openid') === -1) {
- throw new AuthSdkError('openid scope must be specified in the scopes argument when requesting an id_token');
- } else {
- oauthQueryParams.scope = oauthParams.scopes.join(' ');
- }
-
- return oauthQueryParams;
- }
-
- function buildAuthorizeParams(oauthParams) {
- var oauthQueryParams = convertOAuthParamsToQueryParams(oauthParams);
- return util.toQueryParams(oauthQueryParams);
- }
-
- /*
- * Retrieve an idToken from an Okta or a third party idp
- *
- * Two main flows:
- *
- * 1) Exchange a sessionToken for a token
- *
- * Required:
- * clientId: passed via the OktaAuth constructor or into getToken
- * sessionToken: 'yourtoken'
- *
- * Optional:
- * redirectUri: defaults to window.location.href
- * scopes: defaults to ['openid', 'email']
- *
- * Forced:
- * prompt: 'none'
- * responseMode: 'okta_post_message'
- * display: undefined
- *
- * 2) Get a token from an idp
- *
- * Required:
- * clientId: passed via the OktaAuth constructor or into getToken
- *
- * Optional:
- * redirectUri: defaults to window.location.href
- * scopes: defaults to ['openid', 'email']
- * idp: defaults to Okta as an idp
- * prompt: no default. Pass 'none' to throw an error if user is not signed in
- *
- * Forced:
- * display: 'popup'
- *
- * Only common optional params shown. Any OAuth parameters not explicitly forced are available to override
- *
- * @param {Object} oauthOptions
- * @param {String} [oauthOptions.clientId] ID of this client
- * @param {String} [oauthOptions.redirectUri] URI that the iframe or popup will go to once authenticated
- * @param {String[]} [oauthOptions.scopes] OAuth 2.0 scopes to request (openid must be specified)
- * @param {String} [oauthOptions.idp] ID of an external IdP to use for user authentication
- * @param {String} [oauthOptions.sessionToken] Bootstrap Session Token returned by the Okta Authentication API
- * @param {String} [oauthOptions.prompt] Determines whether the Okta login will be displayed on failure.
- * Use 'none' to prevent this behavior
- *
- * @param {Object} options
- * @param {Integer} [options.timeout] Time in ms before the flow is automatically terminated. Defaults to 120000
- * @param {String} [options.popupTitle] Title dispayed in the popup.
- * Defaults to 'External Identity Provider User Authentication'
- */
- function getToken(sdk, oauthOptions, options) {
- oauthOptions = oauthOptions || {};
- options = options || {};
-
- // Default OAuth query params
- var oauthParams = getDefaultOAuthParams(sdk, oauthOptions);
-
- // Start overriding any options that don't make sense
- var sessionTokenOverrides = {
- prompt: 'none',
- responseMode: 'okta_post_message',
- display: null
- };
-
- var idpOverrides = {
- display: 'popup'
- };
-
- if (oauthOptions.sessionToken) {
- util.extend(oauthParams, sessionTokenOverrides);
- } else if (oauthOptions.idp) {
- util.extend(oauthParams, idpOverrides);
- }
-
- // Use the query params to build the authorize url
- var requestUrl,
- urls;
- try {
- // Get authorizeUrl and issuer
- urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options);
- requestUrl = urls.authorizeUrl + buildAuthorizeParams(oauthParams);
- } catch (e) {
- return Q.reject(e);
- }
-
- // Determine the flow type
- var flowType;
- if (oauthParams.sessionToken || oauthParams.display === null) {
- flowType = 'IFRAME';
- } else if (oauthParams.display === 'popup') {
- flowType = 'POPUP';
- } else {
- flowType = 'IMPLICIT';
- }
-
- function getOrigin(url) {
- var originRegex = /^(https?\:\/\/)?([^:\/?#]*(?:\:[0-9]+)?)/;
- return originRegex.exec(url)[0];
- }
-
- // Execute the flow type
- switch (flowType) {
- case 'IFRAME':
- var iframePromise = addPostMessageListener(sdk, options.timeout, oauthParams.state);
- var iframeEl = oauthUtil.loadFrame(requestUrl);
- return iframePromise
- .then(function(res) {
- return handleOAuthResponse(sdk, oauthParams, res, urls);
- })
- .fin(function() {
- if (document.body.contains(iframeEl)) {
- iframeEl.parentElement.removeChild(iframeEl);
- }
- });
-
- case 'POPUP': // eslint-disable-line no-case-declarations
- var popupPromise;
-
- // Add listener on postMessage before window creation, so
- // postMessage isn't triggered before we're listening
- if (oauthParams.responseMode === 'okta_post_message') {
- if (!sdk.features.isPopupPostMessageSupported()) {
- return Q.reject(new AuthSdkError('This browser doesn\'t have full postMessage support'));
- }
- popupPromise = addPostMessageListener(sdk, options.timeout, oauthParams.state);
- }
-
- // Create the window
- var windowOptions = {
- popupTitle: options.popupTitle
- };
- var windowEl = oauthUtil.loadPopup(requestUrl, windowOptions);
-
- // Poll until we get a valid hash fragment
- if (oauthParams.responseMode === 'fragment') {
- var windowOrigin = getOrigin(sdk.idToken.authorize._getLocationHref());
- var redirectUriOrigin = getOrigin(oauthParams.redirectUri);
- if (windowOrigin !== redirectUriOrigin) {
- return Q.reject(new AuthSdkError('Using fragment, the redirectUri origin (' + redirectUriOrigin +
- ') must match the origin of this page (' + windowOrigin + ')'));
- }
- popupPromise = addFragmentListener(sdk, windowEl, options.timeout);
- }
-
- // Both postMessage and fragment require a poll to see if the popup closed
- var popupDeferred = Q.defer();
- function hasClosed(win) { // eslint-disable-line no-inner-declarations
- if (win.closed) {
- popupDeferred.reject(new AuthSdkError('Unable to parse OAuth flow response'));
- }
- }
- var closePoller = setInterval(function() {
- hasClosed(windowEl);
- }, 500);
-
- // Proxy the promise results into the deferred
- popupPromise
- .then(function(res) {
- popupDeferred.resolve(res);
- })
- .fail(function(err) {
- popupDeferred.reject(err);
- });
-
- return popupDeferred.promise
- .then(function(res) {
- return handleOAuthResponse(sdk, oauthParams, res, urls);
- })
- .fin(function() {
- if (!windowEl.closed) {
- clearInterval(closePoller);
- windowEl.close();
- }
- });
-
- default:
- return Q.reject(new AuthSdkError('The full page redirect flow is not supported'));
- }
- }
-
- function getWithoutPrompt(sdk, oauthOptions, options) {
- var oauthParams = util.clone(oauthOptions) || {};
- util.extend(oauthParams, {
- prompt: 'none',
- responseMode: 'okta_post_message',
- display: null
- });
- return getToken(sdk, oauthParams, options);
- }
-
- function getWithPopup(sdk, oauthOptions, options) {
- var oauthParams = util.clone(oauthOptions) || {};
- util.extend(oauthParams, {
- display: 'popup'
- });
- return getToken(sdk, oauthParams, options);
- }
-
- function getWithRedirect(sdk, oauthOptions, options) {
- oauthOptions = util.clone(oauthOptions) || {};
- var oauthParams = getDefaultOAuthParams(sdk, oauthOptions);
- // If the user didn't specify a responseMode
- if (!oauthOptions.responseMode) {
- // And it's only an auth code request (responseType could be an array)
- var respType = oauthParams.responseType;
- if (respType.indexOf('code') !== -1 &&
- (util.isString(respType) || (Array.isArray(respType) && respType.length === 1))) {
- // Default the responseMode to query
- util.extend(oauthParams, {
- responseMode: 'query'
- });
- // Otherwise, default to fragment
- } else {
- util.extend(oauthParams, {
- responseMode: 'fragment'
- });
- }
- }
-
- var urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options);
- var requestUrl = urls.authorizeUrl + buildAuthorizeParams(oauthParams);
-
- // Set session cookie to store the oauthParams
- cookies.setCookie(config.REDIRECT_OAUTH_PARAMS_COOKIE_NAME, JSON.stringify({
- responseType: oauthParams.responseType,
- state: oauthParams.state,
- nonce: oauthParams.nonce,
- scopes: oauthParams.scopes,
- urls: urls
- }));
-
- // Set nonce cookie for servers to validate nonce in id_token
- cookies.setCookie(config.REDIRECT_NONCE_COOKIE_NAME, oauthParams.nonce);
-
- // Set state cookie for servers to validate state
- cookies.setCookie(config.REDIRECT_STATE_COOKIE_NAME, oauthParams.state);
-
- sdk.token.getWithRedirect._setLocation(requestUrl);
- }
-
- function refreshToken(sdk, token) {
- if (!oauthUtil.isToken(token)) {
- return Q.reject(new AuthSdkError('Refresh must be passed a token with ' +
- 'an array of scopes and an accessToken or idToken'));
- }
-
- var responseType;
- if (token.accessToken) {
- responseType = 'token';
- } else {
- responseType = 'id_token';
- }
- return sdk.token.getWithoutPrompt({
- responseType: responseType,
- scopes: token.scopes
- }, {
- authorizeUrl: token.authorizeUrl,
- userinfoUrl: token.userinfoUrl,
- issuer: token.issuer
- });
- }
-
- function removeHash(sdk) {
- var nativeHistory = sdk.token.parseFromUrl._getHistory();
- var nativeDoc = sdk.token.parseFromUrl._getDocument();
- var nativeLoc = sdk.token.parseFromUrl._getLocation();
- if (nativeHistory && nativeHistory.replaceState) {
- nativeHistory.replaceState(null, nativeDoc.title, nativeLoc.pathname + nativeLoc.search);
- } else {
- nativeLoc.hash = '';
- }
- }
-
- function parseFromUrl(sdk, url) {
- var nativeLoc = sdk.token.parseFromUrl._getLocation();
- var hash = nativeLoc.hash;
- if (url) {
- hash = url.substring(url.indexOf('#'));
- }
-
- var oauthParamsCookie = cookies.getCookie(config.REDIRECT_OAUTH_PARAMS_COOKIE_NAME);
- if (!hash || !oauthParamsCookie) {
- return Q.reject(new AuthSdkError('Unable to parse a token from the url'));
- }
- try {
- var oauthParams = JSON.parse(oauthParamsCookie);
- var urls = oauthParams.urls;
- delete oauthParams.urls;
- cookies.deleteCookie(config.REDIRECT_OAUTH_PARAMS_COOKIE_NAME);
- } catch(e) {
- return Q.reject(new AuthSdkError('Unable to parse the ' +
- config.REDIRECT_OAUTH_PARAMS_COOKIE_NAME + ' cookie: ' + e.message));
- }
-
- return Q.resolve(oauthUtil.hashToObject(hash))
- .then(function(res) {
- if (!url) {
- // Remove the hash from the url
- removeHash(sdk);
- }
- return handleOAuthResponse(sdk, oauthParams, res, urls);
- });
- }
-
- function getUserInfo(sdk, accessTokenObject) {
- if (!accessTokenObject ||
- (!oauthUtil.isToken(accessTokenObject) && !accessTokenObject.accessToken && !accessTokenObject.userinfoUrl)) {
- return Q.reject(new AuthSdkError('getUserInfo requires an access token object'));
- }
- return http.httpRequest(sdk, {
- url: accessTokenObject.userinfoUrl,
- method: 'GET',
- accessToken: accessTokenObject.accessToken
- })
- .fail(function(err) {
- if (err.xhr && (err.xhr.status === 401 || err.xhr.status === 403)) {
- var authenticateHeader = err.xhr.getResponseHeader('WWW-Authenticate');
- if (authenticateHeader) {
- var errorMatches = authenticateHeader.match(/error="(.*?)"/) || [];
- var errorDescriptionMatches = authenticateHeader.match(/error_description="(.*?)"/) || [];
- var error = errorMatches[1];
- var errorDescription = errorDescriptionMatches[1];
- if (error && errorDescription) {
- err = new OAuthError(error, errorDescription);
- }
- }
- }
- throw err;
- });
- }
-
- module.exports = {
- getToken: getToken,
- getWithoutPrompt: getWithoutPrompt,
- getWithPopup: getWithPopup,
- getWithRedirect: getWithRedirect,
- parseFromUrl: parseFromUrl,
- refreshIdToken: refreshIdToken,
- decodeToken: decodeToken,
- verifyIdToken: verifyIdToken,
- refreshToken: refreshToken,
- getUserInfo: getUserInfo,
- verifyToken: verifyToken
- };
|