diff --git a/lib/clientBuilder.js b/lib/clientBuilder.js index 5a5e52e46b..917c739120 100644 --- a/lib/clientBuilder.js +++ b/lib/clientBuilder.js @@ -80,8 +80,20 @@ function OktaAuthBuilder(args) { sdk.token = { getWithoutPrompt: util.bind(token.getWithoutPrompt, sdk, sdk), getWithPopup: util.bind(token.getWithPopup, sdk, sdk), + getWithRedirect: util.bind(token.getWithRedirect, sdk, sdk), + parseFromUrl: util.bind(token.parseFromUrl, sdk, sdk), decode: util.bind(token.decodeToken, sdk) }; + + // This is exposed so we can set window.location in our tests + sdk.token.getWithRedirect._setLocation = function(url) { + window.location = url; + }; + + // This is exposed so we can mock window.location.hash in our tests + sdk.token.parseFromUrl._getLocationHash = function(url) { + return window.location.hash; + }; } var proto = OktaAuthBuilder.prototype; diff --git a/lib/cookies.js b/lib/cookies.js index d089ea1b7b..aec9f2f9e7 100644 --- a/lib/cookies.js +++ b/lib/cookies.js @@ -7,14 +7,19 @@ function setCookie(name, value, expiresAt) { } var cookieText = name + '=' + value + ';' + expiresText; - document.cookie = cookieText; + setCookie._setDocumentCookie(cookieText); return cookieText; } +// Exposed for testing +setCookie._setDocumentCookie = function(cookieText) { + document.cookie = cookieText; +}; + function getCookie(name) { var pattern = new RegExp(name + '=([^;]*)'), - matched = document.cookie.match(pattern); + matched = getCookie._getDocumentCookie().match(pattern); if (matched) { var cookie = matched[1]; @@ -22,6 +27,11 @@ function getCookie(name) { } } +// Exposed for testing +getCookie._getDocumentCookie = function() { + return document.cookie; +}; + function deleteCookie(name) { setCookie(name, '', '1970-01-01T00:00:00Z'); } diff --git a/lib/token.js b/lib/token.js index bd7c9aefd3..b736d1595a 100644 --- a/lib/token.js +++ b/lib/token.js @@ -6,6 +6,7 @@ var sdkCrypto = require('./crypto'); var AuthSdkError = require('./errors/AuthSdkError'); var OAuthError = require('./errors/OAuthError'); var config = require('./config.json'); +var cookies = require('./cookies'); function getWellKnown(sdk) { return http.get(sdk, sdk.options.url + '/.well-known/openid-configuration'); @@ -190,37 +191,37 @@ function addPostMessageListener(sdk, timeout) { }); } -function addFragmentListener(sdk, windowEl, timeout) { - var deferred = Q.defer(); - +function hashToObject(hash) { // Predefine regexs for parsing hash var plus2space = /\+/g; var paramSplit = /([^&=]+)=?([^&]*)/g; - function hashToObject(hash) { - // Remove the leading hash - var fragment = hash.substring(1); + // Remove the leading hash + var fragment = hash.substring(1); - var obj = {}; + var obj = {}; - // Loop until we have no more params - var param; - while (true) { // eslint-disable-line no-constant-condition - param = paramSplit.exec(fragment); - if (!param) { break; } + // Loop until we have no more params + var param; + while (true) { // eslint-disable-line no-constant-condition + param = paramSplit.exec(fragment); + if (!param) { break; } - var key = param[1]; - var value = param[2]; + var key = param[1]; + var value = param[2]; - // id_token should remain base64url encoded - if (key === 'id_token') { - obj[key] = value; - } else { - obj[key] = decodeURIComponent(value.replace(plus2space, ' ')); - } + // id_token should remain base64url encoded + if (key === 'id_token' || key === 'access_token') { + obj[key] = value; + } else { + obj[key] = decodeURIComponent(value.replace(plus2space, ' ')); } - return obj; } + return obj; +} + +function addFragmentListener(sdk, windowEl, timeout) { + var deferred = Q.defer(); function hashChangeHandler() { /* @@ -248,6 +249,129 @@ function addFragmentListener(sdk, windowEl, timeout) { return deferred.promise.timeout(timeout || 120000, new AuthSdkError('OAuth flow timed out')); } +function handleOAuthResponse(sdk, oauthParams, res) { + 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 tokenTypes = oauthParams.responseType; + var scopes = util.clone(oauthParams.scope); + var tokenDict = {}; + + if (res['id_token']) { + var jwt = sdk.idToken.decode(res['id_token']); + if (jwt.payload.nonce !== oauthParams.nonce) { + throw new AuthSdkError('OAuth flow response nonce doesn\'t match request nonce'); + } + + var clientId = oauthParams.clientId || sdk.options.clientId; + validateClaims(jwt.payload, sdk.options.url, clientId); + + var idToken = { + idToken: res['id_token'], + claims: jwt.payload, + expiresAt: jwt.payload.exp, + scopes: scopes + }; + + if (Array.isArray(tokenTypes)) { + tokenDict['id_token'] = idToken; + } else { + return idToken; + } + } + + if (res['access_token']) { + var accessToken = { + accessToken: res['access_token'], + expiresAt: Number(res['expires_in']) + Math.floor(Date.now()/1000), + tokenType: res['token_type'], + scopes: scopes + }; + + if (Array.isArray(tokenTypes)) { + tokenDict['token'] = accessToken; + } else { + return accessToken; + } + } + + if (!tokenDict['token'] && !tokenDict['id_token']) { + throw new AuthSdkError('Unable to parse OAuth flow response'); + } + + var tokens = []; + + // Create token array in the order of the responseType array + for (var t = 0, tl = tokenTypes.length; t < tl; t++) { + var tokenType = tokenTypes[t]; + tokens.push(tokenDict[tokenType]); + } + + return tokens; +} + +function getDefaultOAuthParams(sdk, oauthOptions) { + oauthOptions = util.clone(oauthOptions) || {}; + 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), + scope: ['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 + }); + + if (Array.isArray(oauthQueryParams['response_type'])) { + oauthQueryParams['response_type'] = oauthQueryParams['response_type'].join(' '); + } + + if (oauthParams.scope.indexOf('openid') !== -1) { + oauthQueryParams.scope = oauthParams.scope.join(' '); + } else { + throw new AuthSdkError('openid scope must be specified in the scope argument'); + } + + return oauthQueryParams; +} + +function buildAuthorizeUrl(sdk, oauthParams) { + var oauthQueryParams = convertOAuthParamsToQueryParams(oauthParams); + return sdk.options.url + '/oauth2/v1/authorize' + util.toQueryParams(oauthQueryParams); +} + /* * Retrieve an idToken from an Okta or a third party idp * @@ -308,18 +432,7 @@ function getIdToken(sdk, oauthOptions, options) { } // Default OAuth query params - var oauthParams = { - 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), - scope: ['openid', 'email'] - }; - - // Add user-provided options - util.extend(oauthParams, oauthOptions); + var oauthParams = getDefaultOAuthParams(sdk, oauthOptions); // Start overriding any options that don't make sense var sessionTokenOverrides = { @@ -338,41 +451,8 @@ function getIdToken(sdk, oauthOptions, options) { util.extend(oauthParams, idpOverrides); } - // Quick validation - if (!oauthParams.clientId) { - throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to get an idToken'); - } - - 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 oauthQueryHash = 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 - }); - - if (Array.isArray(oauthQueryHash['response_type'])) { - oauthQueryHash['response_type'] = oauthQueryHash['response_type'].join(' '); - } - - if (oauthParams.scope.indexOf('openid') !== -1) { - oauthQueryHash.scope = oauthParams.scope.join(' '); - } else { - throw new AuthSdkError('openid scope must be specified in the scope argument'); - } - // Use the query params to build the authorize url - var requestUrl = sdk.options.url + '/oauth2/v1/authorize' + util.toQueryParams(oauthQueryHash); + var requestUrl = buildAuthorizeUrl(sdk, oauthParams); // Determine the flow type var flowType; @@ -384,74 +464,6 @@ function getIdToken(sdk, oauthOptions, options) { flowType = 'IMPLICIT'; } - function handleOAuthResponse(res) { - 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'); - } - - // If we're passed an array of tokenTypes, - // we return an array in the order specified. - // Otherwise, we return only the token requested - var tokenTypes = oauthParams.responseType; - var scopes = oauthQueryHash.scope.split(' '); - var tokenDict = {}; - - if (res['id_token']) { - var jwt = sdk.idToken.decode(res['id_token']); - if (jwt.payload.nonce !== oauthParams.nonce) { - throw new AuthSdkError('OAuth flow response nonce doesn\'t match request nonce'); - } - - validateClaims(jwt.payload, sdk.options.url, oauthParams.clientId); - - var idToken = { - idToken: res['id_token'], - claims: jwt.payload, - expiresAt: jwt.payload.exp, - scopes: scopes - }; - - if (Array.isArray(tokenTypes)) { - tokenDict['id_token'] = idToken; - } else { - return idToken; - } - } - - if (res['access_token']) { - var accessToken = { - accessToken: res['access_token'], - expiresAt: Number(res['expires_in']) + Math.floor(Date.now()/1000), - tokenType: res['token_type'], - scopes: scopes - }; - - if (Array.isArray(tokenTypes)) { - tokenDict['token'] = accessToken; - } else { - return accessToken; - } - } - - if (!tokenDict['token'] && !tokenDict['id_token']) { - throw new AuthSdkError('Unable to parse OAuth flow response'); - } - - var tokens = []; - - // Create token array in the order of the responseType array - for (var t = 0, tl = tokenTypes.length; t < tl; t++) { - var tokenType = tokenTypes[t]; - tokens.push(tokenDict[tokenType]); - } - - return tokens; - } - function getOrigin(url) { var originRegex = /^(https?\:\/\/)?([^:\/?#]*(?:\:[0-9]+)?)/; return originRegex.exec(url)[0]; @@ -463,7 +475,9 @@ function getIdToken(sdk, oauthOptions, options) { var iframePromise = addPostMessageListener(sdk, options.timeout); var iframeEl = loadFrame(requestUrl, config.FRAME_ID); return iframePromise - .then(handleOAuthResponse) + .then(function(res) { + return handleOAuthResponse(sdk, oauthParams, res); + }) .fin(function() { if (document.body.contains(iframeEl)) { iframeEl.parentElement.removeChild(iframeEl); @@ -520,7 +534,9 @@ function getIdToken(sdk, oauthOptions, options) { }); return popupDeferred.promise - .then(handleOAuthResponse) + .then(function(res) { + return handleOAuthResponse(sdk, oauthParams, res); + }) .fin(function() { if (!windowEl.closed) { clearInterval(closePoller); @@ -551,10 +567,54 @@ function getWithPopup(sdk, oauthOptions, options) { return getIdToken(sdk, oauthParams, options); } +function getWithRedirect(sdk, oauthOptions, options) { + oauthOptions = util.clone(oauthOptions) || {}; + var oauthParams = getDefaultOAuthParams(sdk, oauthOptions); + util.extend(oauthParams, { + responseMode: 'fragment' + }); + var requestUrl = buildAuthorizeUrl(sdk, 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, + scope: oauthParams.scope + })); + + sdk.token.getWithRedirect._setLocation(requestUrl); +} + +function parseFromUrl(sdk, url) { + var hash = sdk.token.parseFromUrl._getLocationHash(); + 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); + } catch(e) { + return Q.reject(new AuthSdkError('Unable to parse the ' + + config.REDIRECT_OAUTH_PARAMS_COOKIE_NAME + ' cookie: ' + e.message)); + } + + return Q.resolve(hashToObject(hash)) + .then(function(res) { + return handleOAuthResponse(sdk, oauthParams, res); + }); +} + module.exports = { getIdToken: getIdToken, getWithoutPrompt: getWithoutPrompt, getWithPopup: getWithPopup, + getWithRedirect: getWithRedirect, + parseFromUrl: parseFromUrl, refreshIdToken: refreshIdToken, decodeToken: decodeToken, verifyIdToken: verifyIdToken diff --git a/package.json b/package.json index 35718e9c86..3fbd372c55 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "okta-auth-js": { "STATE_TOKEN_COOKIE_NAME": "oktaStateToken", "DEFAULT_POLLING_DELAY": 500, - "FRAME_ID": "okta-oauth-helper-frame" + "FRAME_ID": "okta-oauth-helper-frame", + "REDIRECT_OAUTH_PARAMS_COOKIE_NAME": "okta-oauth-redirect-params" } } diff --git a/test/spec/oauth.js b/test/spec/oauth.js index 7c803c8929..f523ac3532 100644 --- a/test/spec/oauth.js +++ b/test/spec/oauth.js @@ -260,9 +260,9 @@ define(function(require) { }, { name: 'AuthSdkError', - message: 'A clientId must be specified in the OktaAuth constructor to get an idToken', + message: 'A clientId must be specified in the OktaAuth constructor to get a token', errorCode: 'INTERNAL', - errorSummary: 'A clientId must be specified in the OktaAuth constructor to get an idToken', + errorSummary: 'A clientId must be specified in the OktaAuth constructor to get a token', errorLink: 'INTERNAL', errorId: 'INTERNAL', errorCauses: [] diff --git a/test/spec/token.js b/test/spec/token.js index 0d7853d838..f28cc4d692 100644 --- a/test/spec/token.js +++ b/test/spec/token.js @@ -439,4 +439,252 @@ define(function(require) { }); }); }); + + describe('token.getWithRedirect', function() { + it('sets authorize url and cookie for id_token using sessionToken', function() { + oauthUtil.setupRedirect({ + getWithRedirectArgs: { + sessionToken: 'testToken' + }, + expectedCookie: 'okta-oauth-redirect-params=' + JSON.stringify({ + responseType: 'id_token', + state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + scope: ['openid', 'email'] + }) + ';', + expectedRedirectUrl: 'https://lboyette.trexcloud.com/oauth2/v1/authorize?' + + 'client_id=NPSfOkH5eZrTy8PMDlvx&' + + 'redirect_uri=https%3A%2F%2Flboyette.trexcloud.com%2Fredirect&' + + 'response_type=id_token&' + + 'response_mode=fragment&' + + 'state=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&' + + 'nonce=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&' + + 'sessionToken=testToken&' + + 'scope=openid%20email' + }); + }); + + it('sets authorize url for access_token using sessionToken', function() { + oauthUtil.setupRedirect({ + getWithRedirectArgs: { + responseType: 'token', + sessionToken: 'testToken' + }, + expectedCookie: 'okta-oauth-redirect-params=' + JSON.stringify({ + responseType: 'token', + state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + scope: ['openid', 'email'] + }) + ';', + expectedRedirectUrl: 'https://lboyette.trexcloud.com/oauth2/v1/authorize?' + + 'client_id=NPSfOkH5eZrTy8PMDlvx&' + + 'redirect_uri=https%3A%2F%2Flboyette.trexcloud.com%2Fredirect&' + + 'response_type=token&' + + 'response_mode=fragment&' + + 'state=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&' + + 'nonce=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&' + + 'sessionToken=testToken&' + + 'scope=openid%20email' + }); + }); + + it('sets authorize url for access_token and id_token using idp', function() { + oauthUtil.setupRedirect({ + getWithRedirectArgs: { + responseType: ['token', 'id_token'], + idp: 'testIdp' + }, + expectedCookie: 'okta-oauth-redirect-params=' + JSON.stringify({ + responseType: ['token', 'id_token'], + state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + scope: ['openid', 'email'] + }) + ';', + expectedRedirectUrl: 'https://lboyette.trexcloud.com/oauth2/v1/authorize?' + + 'client_id=NPSfOkH5eZrTy8PMDlvx&' + + 'redirect_uri=https%3A%2F%2Flboyette.trexcloud.com%2Fredirect&' + + 'response_type=token%20id_token&' + + 'response_mode=fragment&' + + 'state=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&' + + 'nonce=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&' + + 'idp=testIdp&' + + 'scope=openid%20email' + }); + }); + }); + + describe('token.parseFromUrl', function() { + it('parses id_token', function(done) { + return oauthUtil.setupParseUrl({ + hashMock: '#id_token=' + tokens.standardIdToken + + '&state=' + oauthUtil.mockedState, + oauthCookie: 'okta-oauth-redirect-params=' + JSON.stringify({ + responseType: 'id_token', + state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + scope: ['openid', 'email'] + }) + ';', + expectedResp: { + idToken: tokens.standardIdToken, + claims: tokens.standardIdTokenClaims, + expiresAt: 1449699930, + scopes: ['openid', 'email'] + } + }) + .fin(function() { + done(); + }); + }); + + it('parses access_token', function(done) { + return oauthUtil.setupParseUrl({ + time: 1449699929, + hashMock: '#access_token=' + tokens.standardAccessToken + + '&expires_in=3600' + + '&token_type=Bearer' + + '&state=' + oauthUtil.mockedState, + oauthCookie: 'okta-oauth-redirect-params=' + JSON.stringify({ + responseType: 'token', + state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + scope: ['openid', 'email'] + }) + ';', + expectedResp: { + accessToken: tokens.standardAccessToken, + expiresAt: 1449703529, + scopes: ['openid', 'email'], + tokenType: 'Bearer' + } + }) + .fin(function() { + done(); + }); + }); + + it('parses access_token and id_token', function(done) { + return oauthUtil.setupParseUrl({ + time: 1449699929, + hashMock: '#access_token=' + tokens.standardAccessToken + + '&id_token=' + tokens.standardIdToken + + '&expires_in=3600' + + '&token_type=Bearer' + + '&state=' + oauthUtil.mockedState, + oauthCookie: 'okta-oauth-redirect-params=' + JSON.stringify({ + responseType: ['id_token', 'token'], + state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + scope: ['openid', 'email'] + }) + ';', + expectedResp: [{ + idToken: tokens.standardIdToken, + claims: tokens.standardIdTokenClaims, + expiresAt: 1449699930, + scopes: ['openid', 'email'] + }, { + accessToken: tokens.standardAccessToken, + expiresAt: 1449703529, + scopes: ['openid', 'email'], + tokenType: 'Bearer' + }] + }) + .fin(function() { + done(); + }); + }); + + oauthUtil.itpErrorsCorrectly('throws an error if nothing to parse', + { + setupMethod: oauthUtil.setupParseUrl, + hashMock: '', + oauthCookie: 'okta-oauth-redirect-params=' + JSON.stringify({ + responseType: ['id_token', 'token'], + state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + scope: ['openid', 'email'] + }) + ';' + }, + { + name: 'AuthSdkError', + message: 'Unable to parse a token from the url', + errorCode: 'INTERNAL', + errorSummary: 'Unable to parse a token from the url', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + } + ); + + oauthUtil.itpErrorsCorrectly('throws an error if no cookie set', + { + setupMethod: oauthUtil.setupParseUrl, + hashMock: '#access_token=' + tokens.standardAccessToken + + '&id_token=' + tokens.standardIdToken + + '&expires_in=3600' + + '&token_type=Bearer' + + '&state=' + oauthUtil.mockedState, + oauthCookie: '' + }, + { + name: 'AuthSdkError', + message: 'Unable to parse a token from the url', + errorCode: 'INTERNAL', + errorSummary: 'Unable to parse a token from the url', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + } + ); + + oauthUtil.itpErrorsCorrectly('throws an error if state doesn\'t match', + { + setupMethod: oauthUtil.setupParseUrl, + hashMock: '#access_token=' + tokens.standardAccessToken + + '&id_token=' + tokens.standardIdToken + + '&expires_in=3600' + + '&token_type=Bearer' + + '&state=' + oauthUtil.mockedState, + oauthCookie: 'okta-oauth-redirect-params=' + JSON.stringify({ + responseType: ['id_token', 'token'], + state: 'mismatchedState', + nonce: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + scope: ['openid', 'email'] + }) + ';' + }, + { + name: 'AuthSdkError', + message: 'OAuth flow response state doesn\'t match request state', + errorCode: 'INTERNAL', + errorSummary: 'OAuth flow response state doesn\'t match request state', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + } + ); + + oauthUtil.itpErrorsCorrectly('throws an error if nonce doesn\'t match', + { + setupMethod: oauthUtil.setupParseUrl, + hashMock: '#access_token=' + tokens.standardAccessToken + + '&id_token=' + tokens.standardIdToken + + '&expires_in=3600' + + '&token_type=Bearer' + + '&state=' + oauthUtil.mockedState, + oauthCookie: 'okta-oauth-redirect-params=' + JSON.stringify({ + responseType: ['id_token', 'token'], + state: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + nonce: 'mismatchedNonce', + scope: ['openid', 'email'] + }) + ';' + }, + { + name: 'AuthSdkError', + message: 'OAuth flow response nonce doesn\'t match request nonce', + errorCode: 'INTERNAL', + errorSummary: 'OAuth flow response nonce doesn\'t match request nonce', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [] + } + ); + }); }); diff --git a/test/util/oauthUtil.js b/test/util/oauthUtil.js index 3d017a32ec..9158ef1b02 100644 --- a/test/util/oauthUtil.js +++ b/test/util/oauthUtil.js @@ -30,6 +30,34 @@ define(function(require) { scopes: ['openid', 'email'] }; + function getTime(time) { + if (time || time === 0) { + return time; + } else { + return tokens.standardIdTokenClaims.exp - 1; + } + } + + function validateResponse(res, expectedResp) { + function expectResponsesToEqual(actual, expected) { + expect(actual.idToken).toEqual(expected.idToken); + expect(actual.claims).toEqual(expected.claims); + expect(actual.accessToken).toEqual(expected.accessToken); + expect(actual.expiresAt).toEqual(expected.expiresAt); + expect(actual.tokenType).toEqual(expected.tokenType); + } + + if (Array.isArray(expectedResp)) { + expect(res.length).toEqual(expectedResp.length); + var rl = res.length; + while(rl--) { + expectResponsesToEqual(res[rl], expectedResp[rl]); + } + } else { + expectResponsesToEqual(res, expectedResp); + } + } + oauthUtil.setup = function(opts) { if (opts && @@ -63,17 +91,10 @@ define(function(require) { }); } - // Make sure our token isn't expired - var time; - if (opts.time || opts.time === 0) { - time = opts.time; - } else { - time = tokens.standardIdTokenClaims.exp - 1; - } - util.warpToUnixTime(time); + util.warpToUnixTime(getTime(opts.time)); if (opts.hrefMock) { - util.mockWindowLocationHref(authClient, opts.hrefMock); + util.mockGetWindowLocation(authClient, opts.hrefMock); } var promise; @@ -89,24 +110,7 @@ define(function(require) { return promise .then(function(res) { var expectedResp = opts.expectedResp || defaultResponse; - - function expectResponsesToEqual(actual, expected) { - expect(actual.idToken).toEqual(expected.idToken); - expect(actual.claims).toEqual(expected.claims); - expect(actual.accessToken).toEqual(expected.accessToken); - expect(actual.expiresAt).toEqual(expected.expiresAt); - expect(actual.tokenType).toEqual(expected.tokenType); - } - - if (Array.isArray(expectedResp)) { - expect(res.length).toEqual(expectedResp.length); - var rl = res.length; - while(rl--) { - expectResponsesToEqual(res[rl], expectedResp[rl]); - } - } else { - expectResponsesToEqual(res, expectedResp); - } + validateResponse(res, expectedResp); }) .fail(function(err) { if (opts.willFail) { @@ -207,6 +211,41 @@ define(function(require) { } }); }; + + oauthUtil.setupRedirect = function(opts) { + var client = new OktaAuth({ + url: 'https://lboyette.trexcloud.com', + clientId: 'NPSfOkH5eZrTy8PMDlvx', + redirectUri: 'https://lboyette.trexcloud.com/redirect' + }); + + oauthUtil.mockStateAndNonce(); + var windowLocationMock = util.mockSetWindowLocation(client); + var setCookieMock = util.mockSetCookie(); + + client.token.getWithRedirect(opts.getWithRedirectArgs); + + expect(windowLocationMock).toHaveBeenCalledWith(opts.expectedRedirectUrl); + expect(setCookieMock).toHaveBeenCalledWith(opts.expectedCookie); + }; + + oauthUtil.setupParseUrl = function(opts) { + var client = new OktaAuth({ + url: 'https://lboyette.trexcloud.com', + clientId: 'NPSfOkH5eZrTy8PMDlvx', + redirectUri: 'https://lboyette.trexcloud.com/redirect' + }); + + util.warpToUnixTime(getTime(opts.time)); + util.mockGetLocationHash(client, opts.hashMock); + util.mockGetCookie(opts.oauthCookie); + + return client.token.parseFromUrl() + .then(function(res) { + var expectedResp = opts.expectedResp; + validateResponse(res, expectedResp); + }); + }; function expectErrorToEqual(actual, expected) { expect(actual.name).toEqual(expected.name); @@ -241,7 +280,9 @@ define(function(require) { it(title, function (done) { options.willFail = true; var setupMethod; - if (options.authorizeArgs && + if (options.setupMethod) { + setupMethod = options.setupMethod; + } else if (options.authorizeArgs && (options.authorizeArgs.responseMode === 'fragment' || options.authorizeArgs.idp)) { setupMethod = oauthUtil.setupPopup; } else { diff --git a/test/util/util.js b/test/util/util.js index e61a3252c4..edc2de4836 100644 --- a/test/util/util.js +++ b/test/util/util.js @@ -4,7 +4,8 @@ define(function(require) { var Q = require('q'), $ = require('jquery'), _ = require('lodash'), - OktaAuth = require('OktaAuth'); + OktaAuth = require('OktaAuth'), + cookies = require('../../lib/cookies'); function generateXHRPair(request, response, uri) { return Q.Promise(function(resolve) { @@ -314,9 +315,25 @@ define(function(require) { }); }; - util.mockWindowLocationHref = function (client, href) { + util.mockGetWindowLocation = function (client, href) { spyOn(client.idToken.authorize, '_getLocationHref').and.returnValue(href); }; + util.mockSetWindowLocation = function (client) { + return spyOn(client.token.getWithRedirect, '_setLocation'); + }; + + util.mockSetCookie = function () { + return spyOn(cookies.setCookie, '_setDocumentCookie'); + }; + + util.mockGetCookie = function (text) { + spyOn(cookies.getCookie, '_getDocumentCookie').and.returnValue(text || ''); + }; + + util.mockGetLocationHash = function (client, hash) { + spyOn(client.token.parseFromUrl, '_getLocationHash').and.returnValue(hash); + }; + return util; }); diff --git a/webpack.config.js b/webpack.config.js index 8c4a7cf10b..0dd9be912c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,7 +25,7 @@ module.exports = _.extend(commonConfig, { compress: { warnings: false }, - sourceMap: false, + sourceMap: true, comments: function(node, comment) { // Remove other Okta copyrights var isLicense = /^!/.test(comment.value); @@ -36,5 +36,6 @@ module.exports = _.extend(commonConfig, { // Add a single Okta license after removing others new webpack.BannerPlugin(license) - ] + ], + devtool: 'source-map' });