@okta/okta-auth-js
Advanced tools
Comparing version 2.13.2 to 3.0.0
# Changelog | ||
## 3.0.0 | ||
### Features | ||
New [option](README.md#additional-options) `cookies` allows overriding default `secure` and `sameSite` values. | ||
### Breaking Changes | ||
- [#308](https://github.com/okta/okta-auth-js/pull/308) - Removed `jquery` and `reqwest` httpRequesters | ||
- [#309](https://github.com/okta/okta-auth-js/pull/309) - Removed `Q` library, now using standard Promise. IE11 will require a polyfill for the `Promise` object. Use of `Promise.prototype.finally` requires Node > 10.3 for server-side use. | ||
- [#310](https://github.com/okta/okta-auth-js/pull/310) - New behavior for [signOut()](README.md#signout) | ||
- `postLogoutRedirectUri` will default to `window.location.origin` | ||
- [signOut()](README.md#signout) will revoke access token and perform redirect by default. Fallback to XHR [closeSession()](README.md#closesession) if no idToken. | ||
- New method [closeSession()](README.md#closesession) for XHR signout without redirect or reload. | ||
- New method [revokeAccessToken()](README.md#revokeaccesstokenaccesstoken) | ||
- [#311](https://github.com/okta/okta-auth-js/pull/311) - [parseFromUrl()](README.md#tokenparsefromurloptions) now returns tokens in an object hash (instead of array). The `state` parameter (passed to authorize request) is also returned. | ||
- [#313](https://github.com/okta/okta-auth-js/pull/313) - An HTTPS origin will be enforced unless running on `http://localhost` or `cookies.secure` is set to `false` | ||
- [#316](https://github.com/okta/okta-auth-js/pull/316) - Option `issuer` is [required](README.md#configuration-reference). Option `url` has been deprecated and is no longer used. | ||
- [#317](https://github.com/okta/okta-auth-js/pull/317) - `pkce` [option](README.md#additional-options) is now `true` by default. `grantType` option is removed. | ||
- [#320](https://github.com/okta/okta-auth-js/pull/320) - `getWithRedirect`, `getWithPopup`, and `getWithoutPrompt` previously took 2 sets of option objects as parameters, a set of "oauthOptions" and additional options. These methods now take a single options object which can hold all [available options](README.md#authorize-options). Passing a second options object will cause an exception to be thrown. | ||
- [#321](https://github.com/okta/okta-auth-js/pull/321) | ||
- Default responseType when using [implicit flow](README.md#implicit-oauth-20-flow) is now `['token', 'id_token']`. | ||
- When both access token and id token are returned, the id token's `at_hash` claim will be validated against the access token | ||
- [#325](https://github.com/okta/okta-auth-js/pull/325) - Previously, the default `responseMode` for [PKCE](README.md#pkce-oauth-20-flow) was `"fragment"`. It is now `"query"`. Unless explicitly specified using the `responseMode` option, the `response_mode` parameter is no longer passed by `token.getWithRedirect` to the `/authorize` endpoint. The `response_mode` will be set by the backend according to the [OpenID specification](https://openid.net/specs/openid-connect-core-1_0.html#Authentication). [Implicit flow](README.md#implicit-oauth-20-flow) will use `"fragment"` and [PKCE](README.md#pkce-oauth-20-flow) will use `"query"`. If previous behavior is desired, [PKCE](README.md#pkce-oauth-20-flow) can set the `responseMode` option to `"fragment"`. | ||
- [#329](https://github.com/okta/okta-auth-js/pull/329) - Fix internal fetch implementation. `responseText` will always be a string, regardless of headers or response type. If a JSON object was returned, the object will be returned as `responseJSON` and `responseType` will be set to "json". Invalid/malformed JSON server response will no longer throw a raw TypeError but will return a well structured error response which includes the `status` code returned from the server. | ||
### Other | ||
- [#306](https://github.com/okta/okta-auth-js/pull/306) - Now using babel for ES5 compatibility. [All polyfills have been removed](README.md#browser-compatibility). | ||
- [#312](https://github.com/okta/okta-auth-js/pull/312) - Added an E2E test for server-side authentication (node module, not webpack). | ||
## 2.13.2 | ||
@@ -4,0 +46,0 @@ |
@@ -1,3 +0,2 @@ | ||
var packageJson = require('./package.json'); | ||
var OktaAuth = '<rootDir>/' + packageJson.main; | ||
var OktaAuth = '<rootDir>/lib/server/serverIndex.js'; | ||
@@ -16,2 +15,3 @@ module.exports = { | ||
'./test/spec/browserStorage.js', | ||
'./test/spec/cookies.js', | ||
'./test/spec/fingerprint.js', | ||
@@ -18,0 +18,0 @@ './test/spec/general.js', |
@@ -16,5 +16,3 @@ /*! | ||
/* global SDK_VERSION */ | ||
require('../vendor/polyfills'); | ||
/* global window, navigator, document, crypto */ | ||
var Emitter = require('tiny-emitter'); | ||
@@ -27,3 +25,2 @@ var AuthSdkError = require('../errors/AuthSdkError'); | ||
var oauthUtil = require('../oauthUtil'); | ||
var Q = require('q'); | ||
var session = require('../session'); | ||
@@ -38,7 +35,24 @@ var token = require('../token'); | ||
var url = builderUtil.getValidUrl(args); | ||
// OKTA-242989: support for grantType will be removed in 3.0 | ||
var usePKCE = args.pkce || args.grantType === 'authorization_code'; | ||
builderUtil.assertValidConfig(args); | ||
var cookieSettings = util.extend({ | ||
secure: true | ||
}, args.cookies); | ||
var isLocalhost = (sdk.features.isLocalhost() && !sdk.features.isHTTPS()); | ||
if (isLocalhost) { | ||
cookieSettings.secure = false; // Force secure=false if running on http://localhost | ||
} | ||
if (typeof cookieSettings.sameSite === 'undefined') { | ||
// Chrome >= 80 will block cookies with SameSite=None unless they are also Secure | ||
cookieSettings.sameSite = cookieSettings.secure ? 'none' : 'lax'; | ||
} | ||
if (cookieSettings.secure && !sdk.features.isHTTPS()) { | ||
throw new AuthSdkError( | ||
'The current page is not being served with the HTTPS protocol.\n' + | ||
'For security reasons, we strongly recommend using HTTPS.\n' + | ||
'If you cannot use HTTPS, set "cookies.secure" option to false.' | ||
); | ||
} | ||
this.options = { | ||
url: util.removeTrailingSlash(url), | ||
clientId: args.clientId, | ||
@@ -51,3 +65,3 @@ issuer: util.removeTrailingSlash(args.issuer), | ||
logoutUrl: util.removeTrailingSlash(args.logoutUrl), | ||
pkce: usePKCE, | ||
pkce: args.pkce === false ? false : true, | ||
redirectUri: args.redirectUri, | ||
@@ -61,2 +75,3 @@ postLogoutRedirectUri: args.postLogoutRedirectUri, | ||
onSessionExpired: args.onSessionExpired, | ||
cookies: cookieSettings | ||
}; | ||
@@ -209,2 +224,5 @@ | ||
proto.features.isLocalhost = function() { | ||
return window.location.hostname === 'localhost'; | ||
}; | ||
// { username, password, (relayState), (context) } | ||
@@ -231,13 +249,44 @@ proto.signIn = function (opts) { | ||
// Ends the current application session, clearing all local tokens | ||
// Optionally revokes the access token | ||
// Ends the user's Okta session using the API or redirect method | ||
proto.signOut = function (options) { | ||
// Ends the current Okta SSO session without redirecting to Okta. | ||
proto.closeSession = function closeSession() { | ||
var sdk = this; | ||
// Clear all local tokens | ||
sdk.tokenManager.clear(); | ||
return sdk.session.close() // DELETE /api/v1/sessions/me | ||
.catch(function(e) { | ||
if (e.name === 'AuthApiError' && e.errorCode === 'E0000007') { | ||
// Session does not exist or has already been closed | ||
return; | ||
} | ||
throw e; | ||
}); | ||
}; | ||
// Revokes the access token for the application session | ||
proto.revokeAccessToken = async function revokeAccessToken(accessToken) { | ||
var sdk = this; | ||
if (!accessToken) { | ||
accessToken = await sdk.tokenManager.get('accessToken'); | ||
} | ||
// Access token may have been removed. In this case, we will silently succeed. | ||
if (!accessToken) { | ||
return Promise.resolve(); | ||
} | ||
return sdk.token.revoke(accessToken); | ||
}; | ||
// Revokes accessToken, clears all local tokens, then redirects to Okta to end the SSO session. | ||
proto.signOut = async function (options) { | ||
options = util.extend({}, options); | ||
// postLogoutRedirectUri must be whitelisted in Okta Admin UI | ||
var postLogoutRedirectUri = options.postLogoutRedirectUri || this.options.postLogoutRedirectUri; | ||
var defaultUri = window.location.origin; | ||
var postLogoutRedirectUri = options.postLogoutRedirectUri | ||
|| this.options.postLogoutRedirectUri | ||
|| defaultUri; | ||
var accessToken = options.accessToken; | ||
var revokeAccessToken = options.revokeAccessToken; | ||
var revokeAccessToken = options.revokeAccessToken !== false; | ||
var idToken = options.idToken; | ||
@@ -248,85 +297,42 @@ | ||
function getAccessToken() { | ||
return new Q() | ||
.then(function() { | ||
if (revokeAccessToken && typeof accessToken === 'undefined') { | ||
return sdk.tokenManager.get('token'); | ||
} | ||
return accessToken; | ||
}); | ||
if (typeof idToken === 'undefined') { | ||
idToken = await sdk.tokenManager.get('idToken'); | ||
} | ||
function getIdToken() { | ||
return new Q() | ||
.then(function() { | ||
if (postLogoutRedirectUri && typeof idToken === 'undefined') { | ||
return sdk.tokenManager.get('idToken'); | ||
} | ||
return idToken; | ||
}); | ||
if (revokeAccessToken && typeof accessToken === 'undefined') { | ||
accessToken = await sdk.tokenManager.get('token'); | ||
} | ||
function closeSession() { | ||
return sdk.session.close() // DELETE /api/v1/sessions/me | ||
.catch(function(e) { | ||
if (e.name === 'AuthApiError') { | ||
// Most likely cause is session does not exist or has already been closed | ||
// Could also be a network error. Nothing we can do here. | ||
return; | ||
} | ||
throw e; | ||
}); | ||
// Clear all local tokens | ||
sdk.tokenManager.clear(); | ||
if (revokeAccessToken && accessToken) { | ||
await sdk.revokeAccessToken(accessToken); | ||
} | ||
return Q.allSettled([getAccessToken(), getIdToken()]) | ||
.then(function(tokens) { | ||
accessToken = tokens[0].value; | ||
idToken = tokens[1].value; | ||
// Clear all local tokens | ||
sdk.tokenManager.clear(); | ||
if (revokeAccessToken && accessToken) { | ||
return sdk.token.revoke(accessToken) | ||
.catch(function(e) { | ||
if (e.name === 'AuthApiError') { | ||
// Capture and ignore network errors | ||
return; | ||
} | ||
throw e; | ||
}); | ||
} | ||
}) | ||
// No idToken? This can happen if the storage was cleared. | ||
// Fallback to XHR signOut, then redirect to the post logout uri | ||
if (!idToken) { | ||
return sdk.closeSession() // can throw if the user cannot be signed out | ||
.then(function() { | ||
// XHR signOut method | ||
if (!postLogoutRedirectUri) { | ||
return closeSession(); | ||
if (postLogoutRedirectUri === defaultUri) { | ||
window.location.reload(); | ||
} else { | ||
window.location.assign(postLogoutRedirectUri); | ||
} | ||
}); | ||
} | ||
// No idToken? This can happen if the storage was cleared. | ||
// Fallback to XHR signOut, then redirect to the post logout uri | ||
if (!idToken) { | ||
return closeSession() | ||
.catch(function(err) { | ||
// eslint-disable-next-line no-console | ||
console.log('Unhandled exception while closing session', err); | ||
}) | ||
.then(function() { | ||
window.location.assign(postLogoutRedirectUri); | ||
}); | ||
} | ||
// logout redirect using the idToken. | ||
var state = options.state; | ||
var idTokenHint = idToken.idToken; // a string | ||
var logoutUri = logoutUrl + '?id_token_hint=' + encodeURIComponent(idTokenHint) + | ||
'&post_logout_redirect_uri=' + encodeURIComponent(postLogoutRedirectUri); | ||
// logout redirect using the idToken. | ||
var state = options.state; | ||
var idTokenHint = idToken.idToken; // a string | ||
var logoutUri = logoutUrl + '?id_token_hint=' + encodeURIComponent(idTokenHint) + | ||
'&post_logout_redirect_uri=' + encodeURIComponent(postLogoutRedirectUri); | ||
// State allows option parameters to be passed to logout redirect uri | ||
if (state) { | ||
logoutUri += '&state=' + encodeURIComponent(state); | ||
} | ||
window.location.assign(logoutUri); | ||
}); | ||
// State allows option parameters to be passed to logout redirect uri | ||
if (state) { | ||
logoutUri += '&state=' + encodeURIComponent(state); | ||
} | ||
window.location.assign(logoutUri); | ||
}; | ||
@@ -351,41 +357,44 @@ | ||
if (!sdk.features.isFingerprintSupported()) { | ||
return Q.reject(new AuthSdkError('Fingerprinting is not supported on this device')); | ||
return Promise.reject(new AuthSdkError('Fingerprinting is not supported on this device')); | ||
} | ||
var deferred = Q.defer(); | ||
var timeout; | ||
var iframe; | ||
var listener; | ||
var promise = new Promise(function (resolve, reject) { | ||
iframe = document.createElement('iframe'); | ||
iframe.style.display = 'none'; | ||
var iframe = document.createElement('iframe'); | ||
iframe.style.display = 'none'; | ||
listener = function listener(e) { | ||
if (!e || !e.data || e.origin !== sdk.getIssuerOrigin()) { | ||
return; | ||
} | ||
function listener(e) { | ||
if (!e || !e.data || e.origin !== sdk.options.url) { | ||
return; | ||
} | ||
try { | ||
var msg = JSON.parse(e.data); | ||
} catch (err) { | ||
return reject(new AuthSdkError('Unable to parse iframe response')); | ||
} | ||
try { | ||
var msg = JSON.parse(e.data); | ||
} catch (err) { | ||
return deferred.reject(new AuthSdkError('Unable to parse iframe response')); | ||
} | ||
if (!msg) { return; } | ||
if (msg.type === 'FingerprintAvailable') { | ||
return resolve(msg.fingerprint); | ||
} | ||
if (msg.type === 'FingerprintServiceReady') { | ||
e.source.postMessage(JSON.stringify({ | ||
type: 'GetFingerprint' | ||
}), e.origin); | ||
} | ||
}; | ||
oauthUtil.addListener(window, 'message', listener); | ||
if (!msg) { return; } | ||
if (msg.type === 'FingerprintAvailable') { | ||
return deferred.resolve(msg.fingerprint); | ||
} | ||
if (msg.type === 'FingerprintServiceReady') { | ||
e.source.postMessage(JSON.stringify({ | ||
type: 'GetFingerprint' | ||
}), e.origin); | ||
} | ||
} | ||
oauthUtil.addListener(window, 'message', listener); | ||
iframe.src = sdk.getIssuerOrigin() + '/auth/services/devicefingerprint'; | ||
document.body.appendChild(iframe); | ||
iframe.src = sdk.options.url + '/auth/services/devicefingerprint'; | ||
document.body.appendChild(iframe); | ||
timeout = setTimeout(function() { | ||
reject(new AuthSdkError('Fingerprinting timed out')); | ||
}, options.timeout || 15000); | ||
}); | ||
var timeout = setTimeout(function() { | ||
deferred.reject(new AuthSdkError('Fingerprinting timed out')); | ||
}, options.timeout || 15000); | ||
return deferred.promise.fin(function() { | ||
return promise.finally(function() { | ||
clearTimeout(timeout); | ||
@@ -392,0 +401,0 @@ oauthUtil.removeListener(window, 'message', listener); |
@@ -14,5 +14,5 @@ /*! | ||
var fetchRequest = require('../../fetch/fetchRequest'); | ||
var fetchRequest = require('../fetch/fetchRequest'); | ||
var storageUtil = require('./browserStorage'); | ||
module.exports = require('./browser')(storageUtil, fetchRequest); |
@@ -13,6 +13,7 @@ /*! | ||
*/ | ||
/* global localStorage, sessionStorage */ | ||
var Cookies = require('js-cookie'); | ||
var storageBuilder = require('../storageBuilder'); | ||
var constants = require('../constants'); | ||
var AuthSdkError = require('../errors/AuthSdkError'); | ||
@@ -42,3 +43,3 @@ // Building this as an object allows us to mock the functions in our tests | ||
storageUtil.getPKCEStorage = function() { | ||
storageUtil.getPKCEStorage = function(options) { | ||
if (storageUtil.browserHasLocalStorage()) { | ||
@@ -49,9 +50,7 @@ return storageBuilder(storageUtil.getLocalStorage(), constants.PKCE_STORAGE_NAME); | ||
} else { | ||
return storageBuilder(storageUtil.getCookieStorage({ | ||
secure: window.location.protocol === 'https:' | ||
}), constants.PKCE_STORAGE_NAME); | ||
return storageBuilder(storageUtil.getCookieStorage(options), constants.PKCE_STORAGE_NAME); | ||
} | ||
}; | ||
storageUtil.getHttpCache = function() { | ||
storageUtil.getHttpCache = function(options) { | ||
if (storageUtil.browserHasLocalStorage()) { | ||
@@ -62,5 +61,3 @@ return storageBuilder(storageUtil.getLocalStorage(), constants.CACHE_STORAGE_NAME); | ||
} else { | ||
return storageBuilder(storageUtil.getCookieStorage({ | ||
secure: window.location.protocol === 'https:' | ||
}), constants.CACHE_STORAGE_NAME); | ||
return storageBuilder(storageUtil.getCookieStorage(options), constants.CACHE_STORAGE_NAME); | ||
} | ||
@@ -79,5 +76,7 @@ }; | ||
storageUtil.getCookieStorage = function(options) { | ||
options = options || {}; | ||
var secure = options.secure || false; // currently opt-in | ||
var sameSite = options.sameSite || (secure ? 'none' : 'lax'); | ||
const secure = options.secure; | ||
const sameSite = options.sameSite; | ||
if (typeof secure === 'undefined' || typeof sameSite === 'undefined') { | ||
throw new AuthSdkError('getCookieStorage: "secure" and "sameSite" options must be provided'); | ||
} | ||
return { | ||
@@ -121,7 +120,11 @@ getItem: storageUtil.storage.get, | ||
set: function(name, value, expiresAt, options) { | ||
options = options || {}; | ||
const secure = options.secure; | ||
const sameSite = options.sameSite; | ||
if (typeof secure === 'undefined' || typeof sameSite === 'undefined') { | ||
throw new AuthSdkError('storage.set: "secure" and "sameSite" options must be provided'); | ||
} | ||
var cookieOptions = { | ||
path: options.path || '/', | ||
secure: options.secure, | ||
sameSite: options.sameSite | ||
secure, | ||
sameSite | ||
}; | ||
@@ -128,0 +131,0 @@ |
@@ -17,3 +17,4 @@ /*! | ||
function getValidUrl(args) { | ||
// TODO: use @okta/configuration-validation (move module to this monorepo?) | ||
function assertValidConfig(args) { | ||
if (!args) { | ||
@@ -24,23 +25,26 @@ throw new AuthSdkError('No arguments passed to constructor. ' + | ||
var url = args.url; | ||
if (!url) { | ||
var isUrlRegex = new RegExp('^http?s?://.+'); | ||
if (args.issuer && isUrlRegex.test(args.issuer)) { | ||
// Infer the URL from the issuer URL, omitting the /oauth2/{authServerId} | ||
url = args.issuer.split('/oauth2/')[0]; | ||
} else { | ||
throw new AuthSdkError('No url passed to constructor. ' + | ||
'Required usage: new OktaAuth({url: "https://{yourOktaDomain}.com"})'); | ||
} | ||
var issuer = args.issuer; | ||
if (!issuer) { | ||
throw new AuthSdkError('No issuer passed to constructor. ' + | ||
'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com/oauth2/{authServerId}"})'); | ||
} | ||
if (url.indexOf('-admin.') !== -1) { | ||
throw new AuthSdkError('URL passed to constructor contains "-admin" in subdomain. ' + | ||
'Required usage: new OktaAuth({url: "https://{yourOktaDomain}.com})'); | ||
var isUrlRegex = new RegExp('^http?s?://.+'); | ||
if (!isUrlRegex.test(args.issuer)) { | ||
throw new AuthSdkError('Issuer must be a valid URL. ' + | ||
'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com/oauth2/{authServerId}"})'); | ||
} | ||
return url; | ||
if (issuer.indexOf('-admin.') !== -1) { | ||
throw new AuthSdkError('Issuer URL passed to constructor contains "-admin" in subdomain. ' + | ||
'Required usage: new OktaAuth({issuer: "https://{yourOktaDomain}.com})'); | ||
} | ||
} | ||
function addSharedPrototypes(proto) { | ||
proto.getIssuerOrigin = function() { | ||
// Infer the URL from the issuer URL, omitting the /oauth2/{authServerId} | ||
return this.options.issuer.split('/oauth2/')[0]; | ||
}; | ||
// { username, (relayState) } | ||
@@ -95,3 +99,3 @@ proto.forgotPassword = function (opts) { | ||
buildOktaAuth: buildOktaAuth, | ||
getValidUrl: getValidUrl | ||
assertValidConfig: assertValidConfig | ||
}; |
@@ -12,5 +12,16 @@ /*! | ||
*/ | ||
/* global crypto, Uint8Array, TextEncoder */ | ||
var util = require('./util'); | ||
function getOidcHash(str) { | ||
var buffer = new TextEncoder().encode(str); | ||
return crypto.subtle.digest('SHA-256', buffer).then(function(arrayBuffer) { | ||
var intBuffer = new Uint8Array(arrayBuffer); | ||
var firstHalf = intBuffer.slice(0, 16); | ||
var hash = String.fromCharCode.apply(null, firstHalf); | ||
var b64u = util.stringToBase64Url(hash); // url-safe base64 variant | ||
return b64u; | ||
}); | ||
} | ||
function verifyToken(idToken, key) { | ||
@@ -55,3 +66,4 @@ key = util.clone(key); | ||
module.exports = { | ||
getOidcHash: getOidcHash, | ||
verifyToken: verifyToken | ||
}; |
@@ -16,3 +16,2 @@ /*! | ||
var util = require('./util'); | ||
var Q = require('q'); | ||
var AuthApiError = require('./errors/AuthApiError'); | ||
@@ -31,3 +30,3 @@ var constants = require('./constants'); | ||
storage = storageUtil.storage, | ||
httpCache = storageUtil.getHttpCache(); | ||
httpCache = storageUtil.getHttpCache(sdk.options.cookies); | ||
@@ -38,3 +37,3 @@ if (options.cacheResponse) { | ||
if (cachedResponse && Date.now()/1000 < cachedResponse.expiresAt) { | ||
return Q.resolve(cachedResponse.response); | ||
return Promise.resolve(cachedResponse.response); | ||
} | ||
@@ -61,3 +60,3 @@ } | ||
var err, res; | ||
return new Q(sdk.options.httpRequestClient(method, url, ajaxOptions)) | ||
return sdk.options.httpRequestClient(method, url, ajaxOptions) | ||
.then(function(resp) { | ||
@@ -76,3 +75,3 @@ res = resp.responseText; | ||
if (res && res.stateToken && res.expiresAt) { | ||
storage.set(constants.STATE_TOKEN_KEY_NAME, res.stateToken, res.expiresAt); | ||
storage.set(constants.STATE_TOKEN_KEY_NAME, res.stateToken, res.expiresAt, sdk.options.cookies); | ||
} | ||
@@ -89,3 +88,3 @@ | ||
}) | ||
.fail(function(resp) { | ||
.catch(function(resp) { | ||
var serverErr = resp.responseText || {}; | ||
@@ -121,3 +120,3 @@ if (util.isString(serverErr)) { | ||
function get(sdk, url, options) { | ||
url = util.isAbsoluteUrl(url) ? url : sdk.options.url + url; | ||
url = util.isAbsoluteUrl(url) ? url : sdk.getIssuerOrigin() + url; | ||
var getOptions = { | ||
@@ -132,3 +131,3 @@ url: url, | ||
function post(sdk, url, args, options) { | ||
url = util.isAbsoluteUrl(url) ? url : sdk.options.url + url; | ||
url = util.isAbsoluteUrl(url) ? url : sdk.getIssuerOrigin() + url; | ||
var postOptions = { | ||
@@ -135,0 +134,0 @@ url: url, |
@@ -13,3 +13,3 @@ /*! | ||
*/ | ||
/* global window, document */ | ||
/* eslint-disable complexity, max-statements */ | ||
@@ -21,4 +21,2 @@ var http = require('./http'); | ||
var httpCache = storageUtil.getHttpCache(); | ||
function generateState() { | ||
@@ -83,3 +81,3 @@ return util.genRandomString(64); | ||
function getWellKnown(sdk, issuer) { | ||
var authServerUri = (issuer || sdk.options.issuer || sdk.options.url); | ||
var authServerUri = (issuer || sdk.options.issuer); | ||
return http.get(sdk, authServerUri + '/.well-known/openid-configuration', { | ||
@@ -91,2 +89,4 @@ cacheResponse: true | ||
function getKey(sdk, issuer, kid) { | ||
var httpCache = storageUtil.getHttpCache(sdk.options.cookies); | ||
return getWellKnown(sdk, issuer) | ||
@@ -168,3 +168,6 @@ .then(function(wellKnown) { | ||
function getOAuthUrls(sdk, oauthParams, options) { | ||
function getOAuthUrls(sdk, options) { | ||
if (arguments.length > 2) { | ||
throw new AuthSdkError('As of version 3.0, "getOAuthUrls" takes only a single set of options'); | ||
} | ||
options = options || {}; | ||
@@ -180,50 +183,4 @@ | ||
// If an issuer exists but it's not a url, assume it's an authServerId | ||
if (issuer && !(/^https?:/.test(issuer))) { | ||
// Make it a url | ||
issuer = sdk.options.url + '/oauth2/' + issuer; | ||
} | ||
var baseUrl = issuer.indexOf('/oauth2') > 0 ? issuer : issuer + '/oauth2'; | ||
// If an authorizeUrl is supplied without an issuer, and an id_token is requested | ||
if (!issuer && authorizeUrl && | ||
oauthParams.responseType.indexOf('id_token') !== -1) { | ||
// The issuer is ambiguous, so we won't be able to validate the id_token jwt | ||
throw new AuthSdkError('Cannot request idToken with an authorizeUrl without an issuer'); | ||
} | ||
// If a token is requested without an issuer | ||
if (!issuer && oauthParams && oauthParams.responseType.indexOf('token') !== -1) { | ||
// If an authorizeUrl is supplied without a userinfoUrl | ||
if (authorizeUrl && !userinfoUrl) { | ||
// The userinfoUrl is ambiguous, so we won't be able to call getUserInfo | ||
throw new AuthSdkError('Cannot request accessToken with an authorizeUrl without an issuer or userinfoUrl'); | ||
} | ||
// If a userinfoUrl is supplied without a authorizeUrl | ||
if (userinfoUrl && !authorizeUrl) { | ||
// The authorizeUrl is ambiguous, so we won't be able to call the authorize endpoint | ||
throw new AuthSdkError('Cannot request token with an userinfoUrl without an issuer or authorizeUrl'); | ||
} | ||
} | ||
// Default the issuer to our baseUrl | ||
issuer = issuer || sdk.options.url; | ||
// Trim trailing slashes | ||
issuer = util.removeTrailingSlash(issuer); | ||
var baseUrl = issuer; | ||
// A custom auth server issuer looks like: | ||
// https://example.okta.com/oauth2/aus8aus76q8iphupD0h7 | ||
// | ||
// Most orgs have a "default" custom authorization server: | ||
// https://example.okta.com/oauth2/default | ||
var customAuthServerRegex = new RegExp('^https?://.*?/oauth2/.+'); | ||
if (!customAuthServerRegex.test(baseUrl)) { | ||
// Append '/oauth2' if necessary | ||
if (!baseUrl.endsWith('/oauth2')) { | ||
baseUrl += '/oauth2'; | ||
} | ||
} | ||
authorizeUrl = authorizeUrl || baseUrl + '/v1/authorize'; | ||
@@ -230,0 +187,0 @@ userinfoUrl = userinfoUrl || baseUrl + '/v1/userinfo'; |
@@ -13,3 +13,3 @@ /*! | ||
*/ | ||
/* global crypto */ | ||
/* eslint-disable complexity, max-statements */ | ||
@@ -45,4 +45,8 @@ var AuthSdkError = require('./errors/AuthSdkError'); | ||
function getStorage(sdk) { | ||
return sdk.options.storageUtil.getPKCEStorage(sdk.options.cookies); | ||
} | ||
function saveMeta(sdk, meta) { | ||
var storage = sdk.options.storageUtil.getPKCEStorage(); | ||
var storage = getStorage(sdk); | ||
storage.setStorage(meta); | ||
@@ -52,3 +56,3 @@ } | ||
function loadMeta(sdk) { | ||
var storage = sdk.options.storageUtil.getPKCEStorage(); | ||
var storage = getStorage(sdk); | ||
var obj = storage.getStorage(); | ||
@@ -59,3 +63,3 @@ return obj; | ||
function clearMeta(sdk) { | ||
var storage = sdk.options.storageUtil.getPKCEStorage(); | ||
var storage = getStorage(sdk); | ||
storage.clearStorage(); | ||
@@ -62,0 +66,0 @@ } |
@@ -15,4 +15,2 @@ /*! | ||
require('../vendor/polyfills'); | ||
var builderUtil = require('../builderUtil'); | ||
@@ -27,5 +25,5 @@ var SDK_VERSION = require('../../package.json').version; | ||
var url = builderUtil.getValidUrl(args); | ||
builderUtil.assertValidConfig(args); | ||
this.options = { | ||
url: util.removeTrailingSlash(url), | ||
issuer: util.removeTrailingSlash(args.issuer), | ||
httpRequestClient: args.httpRequestClient, | ||
@@ -32,0 +30,0 @@ storageUtil: args.storageUtil, |
@@ -14,5 +14,5 @@ /*! | ||
var fetchRequest = require('../../fetch/fetchRequest'); | ||
var fetchRequest = require('../fetch/fetchRequest'); | ||
var storageUtil = require('./serverStorage'); | ||
module.exports = require('./server')(storageUtil, fetchRequest); |
@@ -13,3 +13,3 @@ /*! | ||
*/ | ||
/* global window */ | ||
var util = require('./util'); | ||
@@ -26,3 +26,3 @@ var http = require('./http'); | ||
}) | ||
.fail(function() { | ||
.catch(function() { | ||
return false; | ||
@@ -47,3 +47,3 @@ }); | ||
}) | ||
.fail(function() { | ||
.catch(function() { | ||
// Return INACTIVE status on failure | ||
@@ -56,3 +56,3 @@ return {status: 'INACTIVE'}; | ||
return http.httpRequest(sdk, { | ||
url: sdk.options.url + '/api/v1/sessions/me', | ||
url: sdk.getIssuerOrigin() + '/api/v1/sessions/me', | ||
method: 'DELETE' | ||
@@ -68,3 +68,3 @@ }); | ||
redirectUrl = redirectUrl || window.location.href; | ||
window.location = sdk.options.url + '/login/sessionCookieRedirect' + | ||
window.location = sdk.getIssuerOrigin() + '/login/sessionCookieRedirect' + | ||
util.toQueryParams({ | ||
@@ -71,0 +71,0 @@ checkAccountSetupComplete: true, |
@@ -43,3 +43,3 @@ /*! | ||
if (!key) { | ||
setStorage({}); | ||
return setStorage({}); | ||
} | ||
@@ -46,0 +46,0 @@ var storage = getStorage(); |
402
lib/token.js
@@ -13,3 +13,3 @@ /*! | ||
*/ | ||
/* global window, document, btoa */ | ||
/* eslint-disable complexity, max-statements */ | ||
@@ -19,3 +19,2 @@ var http = require('./http'); | ||
var oauthUtil = require('./oauthUtil'); | ||
var Q = require('q'); | ||
var sdkCrypto = require('./crypto'); | ||
@@ -30,3 +29,3 @@ var AuthSdkError = require('./errors/AuthSdkError'); | ||
function revokeToken(sdk, token) { | ||
return new Q() | ||
return Promise.resolve() | ||
.then(function() { | ||
@@ -76,3 +75,3 @@ if (!token || !token.accessToken) { | ||
function verifyToken(sdk, token, validationParams) { | ||
return new Q() | ||
return Promise.resolve() | ||
.then(function() { | ||
@@ -87,3 +86,3 @@ if (!token || !token.idToken) { | ||
clientId: sdk.options.clientId, | ||
issuer: sdk.options.issuer || sdk.options.url, | ||
issuer: sdk.options.issuer, | ||
ignoreSignature: sdk.options.ignoreSignature | ||
@@ -111,2 +110,12 @@ }; | ||
} | ||
if (validationParams.accessToken && token.claims.at_hash) { | ||
return sdkCrypto.getOidcHash(validationParams.accessToken) | ||
.then(hash => { | ||
if (hash !== token.claims.at_hash) { | ||
throw new AuthSdkError('Token hash verification failed'); | ||
} | ||
}); | ||
} | ||
}) | ||
.then(() => { | ||
return token; | ||
@@ -118,25 +127,33 @@ }); | ||
function addPostMessageListener(sdk, timeout, state) { | ||
var deferred = Q.defer(); | ||
var responseHandler; | ||
var timeoutId; | ||
var msgReceivedOrTimeout = new Promise(function(resolve, reject) { | ||
function responseHandler(e) { | ||
if (!e.data || e.data.state !== state) { | ||
// A message not meant for us | ||
return; | ||
} | ||
responseHandler = function responseHandler(e) { | ||
if (!e.data || e.data.state !== state) { | ||
// A message not meant for us | ||
return; | ||
} | ||
// Configuration mismatch between saved token and current app instance | ||
// This may happen if apps with different issuers are running on the same host url | ||
// If they share the same storage key, they may read and write tokens in the same location. | ||
// Common when developing against http://localhost | ||
if (e.origin !== sdk.options.url) { | ||
return deferred.reject(new AuthSdkError('The request does not match client configuration')); | ||
} | ||
// Configuration mismatch between saved token and current app instance | ||
// This may happen if apps with different issuers are running on the same host url | ||
// If they share the same storage key, they may read and write tokens in the same location. | ||
// Common when developing against http://localhost | ||
if (e.origin !== sdk.getIssuerOrigin()) { | ||
return reject(new AuthSdkError('The request does not match client configuration')); | ||
} | ||
deferred.resolve(e.data); | ||
} | ||
resolve(e.data); | ||
}; | ||
oauthUtil.addListener(window, 'message', responseHandler); | ||
oauthUtil.addListener(window, 'message', responseHandler); | ||
return deferred.promise.timeout(timeout || 120000, new AuthSdkError('OAuth flow timed out')) | ||
.fin(function() { | ||
timeoutId = setTimeout(function() { | ||
reject(new AuthSdkError('OAuth flow timed out')); | ||
}, timeout || 120000); | ||
}); | ||
return msgReceivedOrTimeout | ||
.finally(function() { | ||
clearTimeout(timeoutId); | ||
oauthUtil.removeListener(window, 'message', responseHandler); | ||
@@ -147,28 +164,35 @@ }); | ||
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) { | ||
var timeoutId; | ||
var promise = new Promise(function(resolve, reject) { | ||
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) { | ||
resolve(oauthUtil.hashToObject(windowEl.location.hash)); | ||
} else if (windowEl && !windowEl.closed) { | ||
setTimeout(hashChangeHandler, 500); | ||
} | ||
} catch (err) { | ||
setTimeout(hashChangeHandler, 500); | ||
} | ||
} catch (err) { | ||
setTimeout(hashChangeHandler, 500); | ||
} | ||
} | ||
hashChangeHandler(); | ||
hashChangeHandler(); | ||
timeoutId = setTimeout(function() { | ||
reject(new AuthSdkError('OAuth flow timed out')); | ||
}, timeout || 120000); | ||
}); | ||
return deferred.promise.timeout(timeout || 120000, new AuthSdkError('OAuth flow timed out')); | ||
return promise.finally(function() { | ||
clearTimeout(timeoutId); | ||
}); | ||
} | ||
@@ -191,3 +215,3 @@ | ||
}) | ||
.fin(function() { | ||
.finally(function() { | ||
PKCE.clearMeta(sdk); | ||
@@ -211,6 +235,10 @@ }); | ||
var responseType = oauthParams.responseType; | ||
if (!Array.isArray(responseType)) { | ||
responseType = [responseType]; | ||
} | ||
var scopes = util.clone(oauthParams.scopes); | ||
var clientId = oauthParams.clientId || sdk.options.clientId; | ||
return new Q() | ||
return Promise.resolve() | ||
.then(function() { | ||
@@ -228,8 +256,13 @@ validateResponse(res, oauthParams); | ||
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'], | ||
var expiresIn = res.expires_in; | ||
var tokenType = res.token_type; | ||
var accessToken = res.access_token; | ||
var idToken = res.id_token; | ||
if (accessToken) { | ||
tokenDict.accessToken = { | ||
value: accessToken, | ||
accessToken: accessToken, | ||
expiresAt: Number(expiresIn) + Math.floor(Date.now()/1000), | ||
tokenType: tokenType, | ||
scopes: scopes, | ||
@@ -241,7 +274,8 @@ authorizeUrl: urls.authorizeUrl, | ||
if (res['id_token']) { | ||
var jwt = sdk.token.decode(res['id_token']); | ||
if (idToken) { | ||
var jwt = sdk.token.decode(idToken); | ||
var idToken = { | ||
idToken: res['id_token'], | ||
var idTokenObj = { | ||
value: idToken, | ||
idToken: idToken, | ||
claims: jwt.payload, | ||
@@ -258,3 +292,4 @@ expiresAt: jwt.payload.exp, | ||
issuer: urls.issuer, | ||
nonce: oauthParams.nonce | ||
nonce: oauthParams.nonce, | ||
accessToken: accessToken | ||
}; | ||
@@ -266,5 +301,5 @@ | ||
return verifyToken(sdk, idToken, validationParams) | ||
return verifyToken(sdk, idTokenObj, validationParams) | ||
.then(function() { | ||
tokenDict['id_token'] = idToken; | ||
tokenDict.idToken = idTokenObj; | ||
return tokenDict; | ||
@@ -277,20 +312,16 @@ }); | ||
.then(function(tokenDict) { | ||
if (!Array.isArray(responseType)) { | ||
return tokenDict[responseType]; | ||
// Validate received tokens against requested response types | ||
if (responseType.indexOf('token') !== -1 && !tokenDict.accessToken) { | ||
// eslint-disable-next-line max-len | ||
throw new AuthSdkError('Unable to parse OAuth flow response: response type "token" was requested but "access_token" was not returned.'); | ||
} | ||
if (responseType.indexOf('id_token') !== -1 && !tokenDict.idToken) { | ||
// eslint-disable-next-line max-len | ||
throw new AuthSdkError('Unable to parse OAuth flow response: response type "id_token" was requested but "id_token" was not returned.'); | ||
} | ||
// Validate response against tokenTypes | ||
var validateTokenTypes = ['token', 'id_token']; | ||
validateTokenTypes.filter(function(key) { | ||
return (responseType.indexOf(key) !== -1); | ||
}).forEach(function(key) { | ||
if (!tokenDict[key]) { | ||
throw new AuthSdkError('Unable to parse OAuth flow response: ' + key + ' was not returned.'); | ||
} | ||
}); | ||
// Create token array in the order of the responseType array | ||
return responseType.map(function(item) { | ||
return tokenDict[item]; | ||
}); | ||
return { | ||
tokens: tokenDict, | ||
state: res.state | ||
}; | ||
}); | ||
@@ -301,7 +332,7 @@ } | ||
return { | ||
pkce: sdk.options.pkce || false, | ||
pkce: sdk.options.pkce, | ||
clientId: sdk.options.clientId, | ||
redirectUri: sdk.options.redirectUri || window.location.href, | ||
responseType: 'id_token', | ||
responseMode: 'okta_post_message', | ||
responseType: ['token', 'id_token'], | ||
responseMode: sdk.options.responseMode, | ||
state: oauthUtil.generateState(), | ||
@@ -414,7 +445,9 @@ nonce: oauthUtil.generateNonce(), | ||
*/ | ||
function getToken(sdk, oauthOptions, options) { | ||
oauthOptions = oauthOptions || {}; | ||
function getToken(sdk, options) { | ||
if (arguments.length > 2) { | ||
return Promise.reject(new AuthSdkError('As of version 3.0, "getToken" takes only a single set of options')); | ||
} | ||
options = options || {}; | ||
return prepareOauthParams(sdk, oauthOptions) | ||
return prepareOauthParams(sdk, options) | ||
.then(function(oauthParams) { | ||
@@ -433,5 +466,5 @@ | ||
if (oauthOptions.sessionToken) { | ||
if (options.sessionToken) { | ||
util.extend(oauthParams, sessionTokenOverrides); | ||
} else if (oauthOptions.idp) { | ||
} else if (options.idp) { | ||
util.extend(oauthParams, idpOverrides); | ||
@@ -444,11 +477,8 @@ } | ||
urls; | ||
try { | ||
// Get authorizeUrl and issuer | ||
urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options); | ||
endpoint = oauthOptions.codeVerifier ? urls.tokenUrl : urls.authorizeUrl; | ||
requestUrl = endpoint + buildAuthorizeParams(oauthParams); | ||
} catch (e) { | ||
return Q.reject(e); | ||
} | ||
// Get authorizeUrl and issuer | ||
urls = oauthUtil.getOAuthUrls(sdk, oauthParams); | ||
endpoint = options.codeVerifier ? urls.tokenUrl : urls.authorizeUrl; | ||
requestUrl = endpoint + buildAuthorizeParams(oauthParams); | ||
// Determine the flow type | ||
@@ -479,3 +509,3 @@ var flowType; | ||
}) | ||
.fin(function() { | ||
.finally(function() { | ||
if (document.body.contains(iframeEl)) { | ||
@@ -486,4 +516,4 @@ iframeEl.parentElement.removeChild(iframeEl); | ||
case 'POPUP': // eslint-disable-line no-case-declarations | ||
var popupPromise; | ||
case 'POPUP': | ||
var oauthPromise; // resolves with OAuth response | ||
@@ -494,5 +524,5 @@ // Add listener on postMessage before window creation, so | ||
if (!sdk.features.isPopupPostMessageSupported()) { | ||
return Q.reject(new AuthSdkError('This browser doesn\'t have full postMessage support')); | ||
throw new AuthSdkError('This browser doesn\'t have full postMessage support'); | ||
} | ||
popupPromise = addPostMessageListener(sdk, options.timeout, oauthParams.state); | ||
oauthPromise = addPostMessageListener(sdk, options.timeout, oauthParams.state); | ||
} | ||
@@ -511,38 +541,34 @@ | ||
if (windowOrigin !== redirectUriOrigin) { | ||
return Q.reject(new AuthSdkError('Using fragment, the redirectUri origin (' + redirectUriOrigin + | ||
') must match the origin of this page (' + windowOrigin + ')')); | ||
throw new AuthSdkError('Using fragment, the redirectUri origin (' + redirectUriOrigin + | ||
') must match the origin of this page (' + windowOrigin + ')'); | ||
} | ||
popupPromise = addFragmentListener(sdk, windowEl, options.timeout); | ||
oauthPromise = addFragmentListener(sdk, windowEl, options.timeout); | ||
} | ||
// Both postMessage and fragment require a poll to see if the popup closed | ||
var popupDeferred = Q.defer(); | ||
/* eslint-disable-next-line no-case-declarations, no-inner-declarations */ | ||
function hasClosed(win) { | ||
if (!win || win.closed) { | ||
popupDeferred.reject(new AuthSdkError('Unable to parse OAuth flow response')); | ||
return true; | ||
} | ||
} | ||
var closePoller = setInterval(function() { | ||
if (hasClosed(windowEl)) { | ||
// The popup may be closed without receiving an OAuth response. Setup a poller to monitor the window. | ||
var popupPromise = new Promise(function(resolve, reject) { | ||
var closePoller = setInterval(function() { | ||
if (!windowEl || windowEl.closed) { | ||
clearInterval(closePoller); | ||
reject(new AuthSdkError('Unable to parse OAuth flow response')); | ||
} | ||
}, 100); | ||
// Proxy the OAuth promise results | ||
oauthPromise | ||
.then(function(res) { | ||
clearInterval(closePoller); | ||
} | ||
}, 500); | ||
// Proxy the promise results into the deferred | ||
popupPromise | ||
.then(function(res) { | ||
popupDeferred.resolve(res); | ||
}) | ||
.fail(function(err) { | ||
popupDeferred.reject(err); | ||
resolve(res); | ||
}) | ||
.catch(function(err) { | ||
clearInterval(closePoller); | ||
reject(err); | ||
}); | ||
}); | ||
return popupDeferred.promise | ||
return popupPromise | ||
.then(function(res) { | ||
return handleOAuthResponse(sdk, oauthParams, res, urls); | ||
}) | ||
.fin(function() { | ||
clearInterval(closePoller); | ||
.finally(function() { | ||
if (windowEl && !windowEl.closed) { | ||
@@ -554,3 +580,3 @@ windowEl.close(); | ||
default: | ||
return Q.reject(new AuthSdkError('The full page redirect flow is not supported')); | ||
throw new AuthSdkError('The full page redirect flow is not supported'); | ||
} | ||
@@ -560,5 +586,8 @@ }); | ||
function getWithoutPrompt(sdk, oauthOptions, options) { | ||
var oauthParams = util.clone(oauthOptions) || {}; | ||
util.extend(oauthParams, { | ||
function getWithoutPrompt(sdk, options) { | ||
if (arguments.length > 2) { | ||
return Promise.reject(new AuthSdkError('As of version 3.0, "getWithoutPrompt" takes only a single set of options')); | ||
} | ||
options = util.clone(options) || {}; | ||
util.extend(options, { | ||
prompt: 'none', | ||
@@ -568,29 +597,27 @@ responseMode: 'okta_post_message', | ||
}); | ||
return getToken(sdk, oauthParams, options); | ||
return getToken(sdk, options); | ||
} | ||
function getWithPopup(sdk, oauthOptions, options) { | ||
var oauthParams = util.clone(oauthOptions) || {}; | ||
util.extend(oauthParams, { | ||
function getWithPopup(sdk, options) { | ||
if (arguments.length > 2) { | ||
return Promise.reject(new AuthSdkError('As of version 3.0, "getWithPopup" takes only a single set of options')); | ||
} | ||
options = util.clone(options) || {}; | ||
util.extend(options, { | ||
display: 'popup', | ||
responseMode: 'okta_post_message' | ||
}); | ||
return getToken(sdk, oauthParams, options); | ||
return getToken(sdk, options); | ||
} | ||
function prepareOauthParams(sdk, oauthOptions) { | ||
function prepareOauthParams(sdk, options) { | ||
// clone and prepare options | ||
oauthOptions = util.clone(oauthOptions) || {}; | ||
options = util.clone(options) || {}; | ||
// OKTA-242989: support for grantType will be removed in 3.0 | ||
if (oauthOptions.grantType === 'authorization_code') { | ||
oauthOptions.pkce = true; | ||
} | ||
// build params using defaults + options | ||
var oauthParams = getDefaultOAuthParams(sdk); | ||
util.extend(oauthParams, oauthOptions); | ||
util.extend(oauthParams, options); | ||
if (oauthParams.pkce !== true) { | ||
return Q.resolve(oauthParams); | ||
if (oauthParams.pkce === false) { | ||
return Promise.resolve(oauthParams); | ||
} | ||
@@ -600,3 +627,3 @@ | ||
if (!sdk.features.isPKCESupported()) { | ||
return Q.reject(new AuthSdkError('This browser doesn\'t support PKCE')); | ||
return Promise.reject(new AuthSdkError('This browser doesn\'t support PKCE')); | ||
} | ||
@@ -643,32 +670,13 @@ | ||
function getWithRedirect(sdk, oauthOptions, options) { | ||
oauthOptions = util.clone(oauthOptions) || {}; | ||
function getWithRedirect(sdk, options) { | ||
if (arguments.length > 2) { | ||
return Promise.reject(new AuthSdkError('As of version 3.0, "getWithRedirect" takes only a single set of options')); | ||
} | ||
options = util.clone(options) || {}; | ||
return prepareOauthParams(sdk, oauthOptions) | ||
return prepareOauthParams(sdk, options) | ||
.then(function(oauthParams) { | ||
// Dynamically set the responseMode unless the user has provided one | ||
// Server-side flow requires query. Client-side apps usually prefer fragment. | ||
if (!oauthOptions.responseMode) { | ||
if (oauthParams.responseType.includes('code') && !oauthParams.pkce) { | ||
// server-side flows using authorization_code | ||
oauthParams.responseMode = 'query'; | ||
} else { | ||
// Client-side flow can use fragment or query. This can be configured on the SDK instance. | ||
oauthParams.responseMode = sdk.options.responseMode || 'fragment'; | ||
} | ||
} | ||
var urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options); | ||
var urls = oauthUtil.getOAuthUrls(sdk, options); | ||
var requestUrl = urls.authorizeUrl + buildAuthorizeParams(oauthParams); | ||
// Chrome >= 80 will block cookies with SameSite=None unless they are also Secure | ||
// If the application is running on HTTPS, we can relax 3rd party cookie settings. | ||
// This will allow embedding the app in an iframe (only if it is running on HTTPS protocol) | ||
var isSecure = window.location.protocol === 'https:'; | ||
var cookieSettings = { | ||
secure: isSecure, | ||
sameSite: isSecure ? 'none' : 'lax' | ||
}; | ||
// Set session cookie to store the oauthParams | ||
@@ -683,9 +691,9 @@ cookies.set(constants.REDIRECT_OAUTH_PARAMS_COOKIE_NAME, JSON.stringify({ | ||
ignoreSignature: oauthParams.ignoreSignature | ||
}), null, cookieSettings); | ||
}), null, sdk.options.cookies); | ||
// Set nonce cookie for servers to validate nonce in id_token | ||
cookies.set(constants.REDIRECT_NONCE_COOKIE_NAME, oauthParams.nonce, null, cookieSettings); | ||
cookies.set(constants.REDIRECT_NONCE_COOKIE_NAME, oauthParams.nonce, null, sdk.options.cookies); | ||
// Set state cookie for servers to validate state | ||
cookies.set(constants.REDIRECT_STATE_COOKIE_NAME, oauthParams.state, null, cookieSettings); | ||
cookies.set(constants.REDIRECT_STATE_COOKIE_NAME, oauthParams.state, null, sdk.options.cookies); | ||
@@ -698,3 +706,3 @@ sdk.token.getWithRedirect._setLocation(requestUrl); | ||
if (!oauthUtil.isToken(token)) { | ||
return Q.reject(new AuthSdkError('Renew must be passed a token with ' + | ||
return Promise.reject(new AuthSdkError('Renew must be passed a token with ' + | ||
'an array of scopes and an accessToken or idToken')); | ||
@@ -714,7 +722,11 @@ } | ||
responseType: responseType, | ||
scopes: token.scopes | ||
}, { | ||
scopes: token.scopes, | ||
authorizeUrl: token.authorizeUrl, | ||
userinfoUrl: token.userinfoUrl, | ||
issuer: token.issuer | ||
}) | ||
.then(function(res) { | ||
// Multiple tokens may have come back. Return only the token which was requested. | ||
var tokens = res.tokens; | ||
return token.idToken ? tokens.idToken : tokens.accessToken; | ||
}); | ||
@@ -751,4 +763,7 @@ } | ||
// https://openid.net/specs/openid-connect-core-1_0.html#Authentication | ||
var defaultResponseMode = sdk.options.pkce ? 'query' : 'fragment'; | ||
var url = options.url; | ||
var responseMode = options.responseMode || sdk.options.responseMode || 'fragment'; | ||
var responseMode = options.responseMode || sdk.options.responseMode || defaultResponseMode; | ||
var nativeLoc = sdk.token.parseFromUrl._getLocation(); | ||
@@ -764,3 +779,3 @@ var paramStr; | ||
if (!paramStr) { | ||
return Q.reject(new AuthSdkError('Unable to parse a token from the url')); | ||
return Promise.reject(new AuthSdkError('Unable to parse a token from the url')); | ||
} | ||
@@ -770,3 +785,3 @@ | ||
if (!oauthParamsCookie) { | ||
return Q.reject(new AuthSdkError('Unable to retrieve OAuth redirect params cookie')); | ||
return Promise.reject(new AuthSdkError('Unable to retrieve OAuth redirect params cookie')); | ||
} | ||
@@ -780,7 +795,7 @@ | ||
} catch(e) { | ||
return Q.reject(new AuthSdkError('Unable to parse the ' + | ||
return Promise.reject(new AuthSdkError('Unable to parse the ' + | ||
constants.REDIRECT_OAUTH_PARAMS_COOKIE_NAME + ' cookie: ' + e.message)); | ||
} | ||
return Q.resolve(oauthUtil.urlParamsToObject(paramStr)) | ||
return Promise.resolve(oauthUtil.urlParamsToObject(paramStr)) | ||
.then(function(res) { | ||
@@ -795,7 +810,21 @@ if (!url) { | ||
function getUserInfo(sdk, accessTokenObject) { | ||
async function getUserInfo(sdk, accessTokenObject, idTokenObject) { | ||
// If token objects were not passed, attempt to read from the TokenManager | ||
if (!accessTokenObject) { | ||
accessTokenObject = await sdk.tokenManager.get('accessToken'); | ||
} | ||
if (!idTokenObject) { | ||
idTokenObject = await sdk.tokenManager.get('idToken'); | ||
} | ||
if (!accessTokenObject || | ||
(!oauthUtil.isToken(accessTokenObject) && !accessTokenObject.accessToken && !accessTokenObject.userinfoUrl)) { | ||
return Q.reject(new AuthSdkError('getUserInfo requires an access token object')); | ||
return Promise.reject(new AuthSdkError('getUserInfo requires an access token object')); | ||
} | ||
if (!idTokenObject || | ||
(!oauthUtil.isToken(idTokenObject) && !idTokenObject.idToken)) { | ||
return Promise.reject(new AuthSdkError('getUserInfo requires an ID token object')); | ||
} | ||
return http.httpRequest(sdk, { | ||
@@ -806,3 +835,10 @@ url: accessTokenObject.userinfoUrl, | ||
}) | ||
.fail(function(err) { | ||
.then(userInfo => { | ||
// Only return the userinfo response if subjects match to mitigate token substitution attacks | ||
if (userInfo.sub === idTokenObject.claims.sub) { | ||
return userInfo; | ||
} | ||
return Promise.reject(new AuthSdkError('getUserInfo request was rejected due to token mismatch')); | ||
}) | ||
.catch(function(err) { | ||
if (err.xhr && (err.xhr.status === 401 || err.xhr.status === 403)) { | ||
@@ -809,0 +845,0 @@ var authenticateHeader; |
@@ -13,3 +13,3 @@ /*! | ||
*/ | ||
/* global localStorage, sessionStorage */ | ||
/* eslint complexity:[0,8] max-statements:[0,21] */ | ||
@@ -19,3 +19,2 @@ var util = require('./util'); | ||
var storageUtil = require('./browser/browserStorage'); | ||
var Q = require('q'); | ||
var constants = require('./constants'); | ||
@@ -120,3 +119,3 @@ var storageBuilder = require('./storageBuilder'); | ||
function getAsync(sdk, tokenMgmtRef, storage, key) { | ||
return Q.Promise(function(resolve) { | ||
return new Promise(function(resolve) { | ||
var token = get(storage, key); | ||
@@ -158,3 +157,3 @@ if (!token || !hasExpired(tokenMgmtRef, token)) { | ||
} catch (e) { | ||
return Q.reject(e); | ||
return Promise.reject(e); | ||
} | ||
@@ -167,11 +166,3 @@ | ||
tokenMgmtRef.renewPromise[key] = sdk.token.renew(token) | ||
.then(function(freshTokens) { | ||
var freshToken = freshTokens; | ||
// With PKCE flow we will receive multiple tokens. Find the one we are looking for | ||
if (freshTokens instanceof Array) { | ||
freshToken = freshTokens.find(function(freshToken) { | ||
return (freshToken.idToken && token.idToken) || (freshToken.accessToken && token.accessToken); | ||
}); | ||
} | ||
.then(function(freshToken) { | ||
var oldToken = get(storage, key); | ||
@@ -236,3 +227,3 @@ if (!oldToken) { | ||
case 'cookie': | ||
storageProvider = storageUtil.getCookieStorage(options); | ||
storageProvider = storageUtil.getCookieStorage(sdk.options.cookies); | ||
break; | ||
@@ -239,0 +230,0 @@ case 'memory': |
@@ -17,3 +17,2 @@ /*! | ||
var util = require('./util'); | ||
var Q = require('q'); | ||
var AuthSdkError = require('./errors/AuthSdkError'); | ||
@@ -41,3 +40,3 @@ var AuthPollStopError = require('./errors/AuthPollStopError'); | ||
args = addStateToken(sdk, args); | ||
return http.post(sdk, sdk.options.url + '/api/v1/authn', args); | ||
return http.post(sdk, sdk.getIssuerOrigin() + '/api/v1/authn', args); | ||
} | ||
@@ -53,3 +52,3 @@ | ||
} else { | ||
return Q.reject(new AuthSdkError('No transaction to resume')); | ||
return Promise.reject(new AuthSdkError('No transaction to resume')); | ||
} | ||
@@ -71,3 +70,3 @@ } | ||
} else { | ||
return Q.reject(new AuthSdkError('No transaction to evaluate')); | ||
return Promise.reject(new AuthSdkError('No transaction to evaluate')); | ||
} | ||
@@ -84,3 +83,3 @@ } | ||
// v1 pipeline introspect API | ||
return http.post(sdk, sdk.options.url + '/api/v1/authn/introspect', args); | ||
return http.post(sdk, sdk.getIssuerOrigin() + '/api/v1/authn/introspect', args); | ||
} | ||
@@ -129,3 +128,3 @@ | ||
catch (e) { | ||
return Q.reject(new AuthSdkError('AutoPush resulted in an error.')); | ||
return Promise.reject(new AuthSdkError('AutoPush resulted in an error.')); | ||
} | ||
@@ -141,3 +140,3 @@ } | ||
catch (e) { | ||
return Q.reject(new AuthSdkError('RememberDevice resulted in an error.')); | ||
return Promise.reject(new AuthSdkError('RememberDevice resulted in an error.')); | ||
} | ||
@@ -161,3 +160,3 @@ } | ||
if (!ref.isPolling) { | ||
return Q.reject(new AuthPollStopError()); | ||
return Promise.reject(new AuthPollStopError()); | ||
} | ||
@@ -182,4 +181,3 @@ return pollFn() | ||
// Continue poll | ||
return Q.delay(delay) | ||
.then(recursivePoll); | ||
return util.delay(delay).then(recursivePoll); | ||
@@ -193,3 +191,3 @@ } else { | ||
}) | ||
.fail(function(err) { | ||
.catch(function(err) { | ||
// Exponential backoff, up to 16 seconds | ||
@@ -201,3 +199,3 @@ if (err.xhr && | ||
retryCount++; | ||
return Q.delay(delayLength) | ||
return util.delay(delayLength) | ||
.then(recursivePoll); | ||
@@ -209,3 +207,3 @@ } | ||
return recursivePoll() | ||
.fail(function(err) { | ||
.catch(function(err) { | ||
ref.isPolling = false; | ||
@@ -267,3 +265,3 @@ throw err; | ||
catch (e) { | ||
return Q.reject(new AuthSdkError('AutoPush resulted in an error.')); | ||
return Promise.reject(new AuthSdkError('AutoPush resulted in an error.')); | ||
} | ||
@@ -284,3 +282,3 @@ } | ||
catch (e) { | ||
return Q.reject(new AuthSdkError('RememberDevice resulted in an error.')); | ||
return Promise.reject(new AuthSdkError('RememberDevice resulted in an error.')); | ||
} | ||
@@ -388,3 +386,3 @@ } | ||
this.cancel = function() { | ||
return new Q(new AuthTransaction(sdk)); | ||
return Promise.resolve(new AuthTransaction(sdk)); | ||
}; | ||
@@ -391,0 +389,0 @@ } |
@@ -12,3 +12,4 @@ /*! | ||
*/ | ||
/* eslint-env es6 */ | ||
/* global window, document, btoa, atob, Uint8Array */ | ||
var util = module.exports; | ||
@@ -91,6 +92,2 @@ | ||
util.isArray = function(obj) { | ||
return Object.prototype.toString.call(obj) === '[object Array]'; | ||
}; | ||
util.isoToUTCString = function(str) { | ||
@@ -277,1 +274,7 @@ var parts = str.match(/\d+/g), | ||
}; | ||
util.delay = function(ms) { | ||
return new Promise(function(resolve) { | ||
setTimeout(resolve, ms); | ||
}); | ||
}; |
{ | ||
"name": "@okta/okta-auth-js", | ||
"description": "The Okta Auth SDK", | ||
"version": "2.13.2", | ||
"version": "3.0.0", | ||
"homepage": "https://github.com/okta/okta-auth-js", | ||
@@ -34,2 +34,11 @@ "license": "Apache-2.0", | ||
], | ||
"browserslist": [ | ||
"> 0.1%", | ||
"not safari < 7.1", | ||
"not ie < 11", | ||
"not IE_Mob 11" | ||
], | ||
"engines": { | ||
"node": ">=10.3" | ||
}, | ||
"dependencies": { | ||
@@ -40,4 +49,2 @@ "Base64": "0.3.0", | ||
"node-cache": "^4.2.0", | ||
"q": "1.4.1", | ||
"reqwest": "2.0.5", | ||
"tiny-emitter": "1.1.0", | ||
@@ -47,4 +54,10 @@ "xhr2": "0.1.3" | ||
"devDependencies": { | ||
"@babel/cli": "^7.8.0", | ||
"@babel/core": "^7.8.0", | ||
"@babel/plugin-transform-runtime": "^7.8.3", | ||
"@babel/preset-env": "^7.8.2", | ||
"babel-jest": "^24.9.0", | ||
"babel-loader": "^8.0.6", | ||
"eslint": "5.6.1", | ||
"eslint-plugin-compat": "^3.3.0", | ||
"eslint-plugin-jasmine": "^2.10.1", | ||
@@ -55,3 +68,2 @@ "istanbul-instrumenter-loader": "^3.0.1", | ||
"jest-junit": "^9.0.0", | ||
"jquery": "3.3.1", | ||
"json-loader": "0.5.4", | ||
@@ -67,3 +79,2 @@ "karma": "^4.1.0", | ||
"promise.allsettled": "^1.0.1", | ||
"promise.prototype.finally": "^3.1.1", | ||
"webpack": "^3.0.0" | ||
@@ -76,5 +87,5 @@ }, | ||
"okta": { | ||
"commitSha": "92f6ae3848e81b2c99e7109a4d1a53ab934079ef", | ||
"fullVersion": "2.13.2-20200303023426-92f6ae3" | ||
"commitSha": "1b317b64e0cfa1f64df46b76f16b9322617cb029", | ||
"fullVersion": "3.0.0-20200304182440-1b317b6" | ||
} | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
6
2004
665425
24
33
3316
- Removedq@1.4.1
- Removedreqwest@2.0.5
- Removedq@1.4.1(transitive)
- Removedreqwest@2.0.5(transitive)