@okta/okta-auth-js
Advanced tools
Comparing version 2.5.0 to 2.6.0
@@ -15,8 +15,18 @@ /*! | ||
/* eslint-disable complexity */ | ||
function fetchRequest(method, url, args) { | ||
var body = args.data; | ||
var headers = args.headers || {}; | ||
var contentType = (headers['Content-Type'] || headers['content-type'] || ''); | ||
// JSON encode body (if appropriate) | ||
if (contentType === 'application/json' && body && typeof body !== 'string') { | ||
body = JSON.stringify(body); | ||
} | ||
var fetchPromise = fetch(url, { | ||
method: method, | ||
headers: args.headers, | ||
body: JSON.stringify(args.data), | ||
credentials: 'include' | ||
body: body, | ||
credentials: args.withCredentials === false ? 'omit' : 'include' | ||
}) | ||
@@ -23,0 +33,0 @@ .then(function(response) { |
@@ -19,3 +19,5 @@ var packageJson = require('./package.json'); | ||
'./test/spec/tokenManager.js', | ||
'./test/spec/webfinger.js' | ||
'./test/spec/webfinger.js', | ||
'./test/spec/pkce.js', | ||
'./test/spec/features.js' | ||
], | ||
@@ -22,0 +24,0 @@ 'reporters': [ |
@@ -16,2 +16,3 @@ /*! | ||
function jqueryRequest(method, url, args) { | ||
// TODO: support content-type | ||
var deferred = $.Deferred(); | ||
@@ -24,3 +25,3 @@ $.ajax({ | ||
xhrFields: { | ||
withCredentials: true | ||
withCredentials: args.withCredentials | ||
} | ||
@@ -27,0 +28,0 @@ }) |
@@ -40,2 +40,4 @@ /*! | ||
userinfoUrl: util.removeTrailingSlash(args.userinfoUrl), | ||
tokenUrl: util.removeTrailingSlash(args.tokenUrl), | ||
grantType: args.grantType, | ||
redirectUri: args.redirectUri, | ||
@@ -48,2 +50,6 @@ httpRequestClient: args.httpRequestClient, | ||
if (this.options.grantType === 'authorization_code' && !sdk.features.isPKCESupported()) { | ||
throw new AuthSdkError('This browser doesn\'t support PKCE'); | ||
} | ||
this.userAgent = 'okta-auth-js-' + config.SDK_VERSION; | ||
@@ -156,2 +162,6 @@ | ||
proto.features.isPKCESupported = function() { | ||
return proto.features.isTokenVerifySupported(); | ||
}; | ||
// { username, password, (relayState), (context) } | ||
@@ -158,0 +168,0 @@ proto.signIn = function (opts) { |
@@ -41,2 +41,12 @@ /*! | ||
storageUtil.getPKCEStorage = function() { | ||
if (storageUtil.browserHasLocalStorage()) { | ||
return storageBuilder(storageUtil.getLocalStorage(), config.PKCE_STORAGE_NAME); | ||
} else if (storageUtil.browserHasSessionStorage()) { | ||
return storageBuilder(storageUtil.getSessionStorage(), config.PKCE_STORAGE_NAME); | ||
} else { | ||
return storageBuilder(storageUtil.getCookieStorage(), config.PKCE_STORAGE_NAME); | ||
} | ||
}; | ||
storageUtil.getHttpCache = function() { | ||
@@ -43,0 +53,0 @@ if (storageUtil.browserHasLocalStorage()) { |
@@ -83,2 +83,5 @@ /*! | ||
// Hoist feature detection functions to static type | ||
OktaAuth.features = OktaAuthBuilder.prototype.features; | ||
return OktaAuth; | ||
@@ -85,0 +88,0 @@ }; |
@@ -12,3 +12,4 @@ module.exports = { | ||
"CACHE_STORAGE_NAME": "okta-cache-storage", | ||
"SDK_VERSION": "2.5.0" | ||
"PKCE_STORAGE_NAME": "okta-pkce-storage", | ||
"SDK_VERSION": "2.6.0" | ||
}; |
@@ -27,2 +27,3 @@ /*! | ||
accessToken = options.accessToken, | ||
withCredentials = options.withCredentials !== false, // default value is true | ||
storageUtil = sdk.options.storageUtil, | ||
@@ -53,3 +54,4 @@ storage = storageUtil.storage, | ||
headers: headers, | ||
data: args || undefined | ||
data: args || undefined, | ||
withCredentials: withCredentials | ||
}; | ||
@@ -56,0 +58,0 @@ |
@@ -22,2 +22,10 @@ /*! | ||
function generateState() { | ||
return util.genRandomString(64); | ||
} | ||
function generateNonce() { | ||
return util.genRandomString(64); | ||
} | ||
function isToken(obj) { | ||
@@ -163,2 +171,3 @@ if (obj && | ||
var userinfoUrl = util.removeTrailingSlash(options.userinfoUrl) || sdk.options.userinfoUrl; | ||
var tokenUrl = util.removeTrailingSlash(options.tokenUrl) || sdk.options.tokenUrl; | ||
@@ -207,3 +216,5 @@ // If an issuer exists but it's not a url, assume it's an authServerId | ||
userinfoUrl = userinfoUrl || issuer + '/v1/userinfo'; | ||
// Shared resource server tokenUrls look like: | ||
// https://example.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/token | ||
tokenUrl = tokenUrl || issuer + '/v1/token'; | ||
// Normally looks like: | ||
@@ -218,2 +229,5 @@ // https://example.okta.com | ||
userinfoUrl = userinfoUrl || issuer + '/oauth2/v1/userinfo'; | ||
// Normal tokenUrls look like: | ||
// https://example.okta.com/oauth2/v1/token | ||
tokenUrl = tokenUrl || issuer + '/oauth2/v1/token'; | ||
} | ||
@@ -224,3 +238,4 @@ | ||
authorizeUrl: authorizeUrl, | ||
userinfoUrl: userinfoUrl | ||
userinfoUrl: userinfoUrl, | ||
tokenUrl: tokenUrl | ||
}; | ||
@@ -259,2 +274,4 @@ } | ||
module.exports = { | ||
generateState: generateState, | ||
generateNonce: generateNonce, | ||
getWellKnown: getWellKnown, | ||
@@ -261,0 +278,0 @@ getKey: getKey, |
@@ -18,2 +18,6 @@ /*! | ||
function storageBuilder(webstorage, storageName) { | ||
if (typeof storageName !== 'string' || !storageName.length) { | ||
throw new AuthSdkError('"storageName" is required'); | ||
} | ||
function getStorage() { | ||
@@ -20,0 +24,0 @@ var storageString = webstorage.getItem(storageName); |
433
lib/token.js
@@ -24,2 +24,3 @@ /*! | ||
var cookies = require('./browser/browserStorage').storage; | ||
var pkce = require('./pkce'); | ||
@@ -131,6 +132,37 @@ function decodeToken(token) { | ||
function exchangeCodeForToken(sdk, oauthParams, authorizationCode, urls) { | ||
// PKCE authorization_code flow | ||
// Retrieve saved values and build oauthParams for call to /token | ||
var meta = pkce.loadMeta(sdk); | ||
var getTokenParams = { | ||
clientId: oauthParams.clientId, | ||
grantType: 'authorization_code', | ||
authorizationCode: authorizationCode, | ||
codeVerifier: meta.codeVerifier, | ||
redirectUri: meta.redirectUri | ||
}; | ||
return pkce.getToken(sdk, getTokenParams, urls) | ||
.then(function(res) { | ||
validateResponse(res, getTokenParams); | ||
return res; | ||
}) | ||
.fin(function() { | ||
pkce.clearMeta(sdk); | ||
}); | ||
} | ||
function validateResponse(res, oauthParams) { | ||
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'); | ||
} | ||
} | ||
function handleOAuthResponse(sdk, oauthParams, res, urls) { | ||
urls = urls || {}; | ||
var tokenTypes = oauthParams.responseType; | ||
var responseType = oauthParams.responseType; | ||
var scopes = util.clone(oauthParams.scopes); | ||
@@ -141,10 +173,12 @@ var clientId = oauthParams.clientId || sdk.options.clientId; | ||
.then(function() { | ||
if (res['error'] || res['error_description']) { | ||
throw new OAuthError(res['error'], res['error_description']); | ||
} | ||
validateResponse(res, oauthParams); | ||
if (res.state !== oauthParams.state) { | ||
throw new AuthSdkError('OAuth flow response state doesn\'t match request state'); | ||
// We do not support "hybrid" scenarios where the response includes both a code and a token. | ||
// If the response contains a code it is used immediately to obtain new tokens. | ||
if (res['code']) { | ||
responseType = ['token', 'id_token']; // what we expect the code to provide us | ||
return exchangeCodeForToken(sdk, oauthParams, res['code'], urls); | ||
} | ||
return res; | ||
}).then(function(res) { | ||
var tokenDict = {}; | ||
@@ -163,8 +197,2 @@ | ||
if (res['code']) { | ||
tokenDict['code'] = { | ||
authorizationCode: res['code'] | ||
}; | ||
} | ||
if (res['id_token']) { | ||
@@ -186,6 +214,9 @@ var jwt = sdk.token.decode(res['id_token']); | ||
issuer: urls.issuer, | ||
nonce: oauthParams.nonce, | ||
ignoreSignature: oauthParams.ignoreSignature | ||
nonce: oauthParams.nonce | ||
}; | ||
if (oauthParams.ignoreSignature !== undefined) { | ||
validationParams.ignoreSignature = oauthParams.ignoreSignature; | ||
} | ||
return verifyToken(sdk, idToken, validationParams) | ||
@@ -201,12 +232,18 @@ .then(function() { | ||
.then(function(tokenDict) { | ||
if (!Array.isArray(tokenTypes)) { | ||
return tokenDict[tokenTypes]; | ||
if (!Array.isArray(responseType)) { | ||
return tokenDict[responseType]; | ||
} | ||
if (!tokenDict['token'] && !tokenDict['id_token']) { | ||
throw new AuthSdkError('Unable to parse OAuth flow response'); | ||
} | ||
// 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 tokenTypes.map(function(item) { | ||
return responseType.map(function(item) { | ||
return tokenDict[item]; | ||
@@ -220,9 +257,12 @@ }); | ||
var grantType = sdk.options.grantType || 'implicit'; | ||
var responseType = grantType === 'authorization_code' ? 'code' : 'id_token'; | ||
var defaults = { | ||
grantType: grantType, | ||
clientId: sdk.options.clientId, | ||
redirectUri: sdk.options.redirectUri || window.location.href, | ||
responseType: 'id_token', | ||
responseType: responseType, | ||
responseMode: 'okta_post_message', | ||
state: util.genRandomString(64), | ||
nonce: util.genRandomString(64), | ||
state: oauthUtil.generateState(), | ||
nonce: oauthUtil.generateNonce(), | ||
scopes: ['openid', 'email'], | ||
@@ -232,2 +272,7 @@ ignoreSignature: sdk.options.ignoreSignature | ||
util.extend(defaults, oauthOptions); | ||
if (defaults.grantType === 'authorization_code' && !defaults.codeChallengeMethod) { | ||
defaults.codeChallengeMethod = pkce.DEFAULT_CODE_CHALLENGE_METHOD; | ||
} | ||
return defaults; | ||
@@ -258,3 +303,5 @@ } | ||
'idp': oauthParams.idp, | ||
'max_age': oauthParams.maxAge | ||
'max_age': oauthParams.maxAge, | ||
'code_challenge': oauthParams.codeChallenge, | ||
'code_challenge_method': oauthParams.codeChallengeMethod | ||
}); | ||
@@ -335,128 +382,131 @@ | ||
// Default OAuth query params | ||
var oauthParams = getDefaultOAuthParams(sdk, oauthOptions); | ||
return prepareOauthParams(sdk, oauthOptions) | ||
.then(function(oauthParams) { | ||
// Start overriding any options that don't make sense | ||
var sessionTokenOverrides = { | ||
prompt: 'none', | ||
responseMode: 'okta_post_message', | ||
display: null | ||
}; | ||
// Start overriding any options that don't make sense | ||
var sessionTokenOverrides = { | ||
prompt: 'none', | ||
responseMode: 'okta_post_message', | ||
display: null | ||
}; | ||
var idpOverrides = { | ||
display: 'popup' | ||
}; | ||
var idpOverrides = { | ||
display: 'popup' | ||
}; | ||
if (oauthOptions.sessionToken) { | ||
util.extend(oauthParams, sessionTokenOverrides); | ||
} else if (oauthOptions.idp) { | ||
util.extend(oauthParams, idpOverrides); | ||
} | ||
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); | ||
} | ||
// Use the query params to build the authorize url | ||
var requestUrl, | ||
endpoint, | ||
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); | ||
} | ||
// Determine the flow type | ||
var flowType; | ||
if (oauthParams.sessionToken || oauthParams.display === null) { | ||
flowType = 'IFRAME'; | ||
} else if (oauthParams.display === 'popup') { | ||
flowType = 'POPUP'; | ||
} else { | ||
flowType = 'IMPLICIT'; | ||
} | ||
// 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) { | ||
/* eslint-disable-next-line no-useless-escape */ | ||
var originRegex = /^(https?\:\/\/)?([^:\/?#]*(?:\:[0-9]+)?)/; | ||
return originRegex.exec(url)[0]; | ||
} | ||
function getOrigin(url) { | ||
/* eslint-disable-next-line no-useless-escape */ | ||
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); | ||
} | ||
}); | ||
// 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; | ||
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')); | ||
// 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); | ||
} | ||
popupPromise = addPostMessageListener(sdk, options.timeout, oauthParams.state); | ||
} | ||
// Create the window | ||
var windowOptions = { | ||
popupTitle: options.popupTitle | ||
}; | ||
var windowEl = oauthUtil.loadPopup(requestUrl, windowOptions); | ||
// 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 + ')')); | ||
// 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); | ||
} | ||
popupPromise = 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.closed) { | ||
popupDeferred.reject(new AuthSdkError('Unable to parse OAuth flow response')); | ||
// 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.closed) { | ||
popupDeferred.reject(new AuthSdkError('Unable to parse OAuth flow response')); | ||
} | ||
} | ||
} | ||
var closePoller = setInterval(function() { | ||
hasClosed(windowEl); | ||
}, 500); | ||
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 | ||
// Proxy the promise results into the deferred | ||
popupPromise | ||
.then(function(res) { | ||
return handleOAuthResponse(sdk, oauthParams, res, urls); | ||
popupDeferred.resolve(res); | ||
}) | ||
.fin(function() { | ||
if (!windowEl.closed) { | ||
clearInterval(closePoller); | ||
windowEl.close(); | ||
} | ||
.fail(function(err) { | ||
popupDeferred.reject(err); | ||
}); | ||
default: | ||
return Q.reject(new AuthSdkError('The full page redirect flow is not supported')); | ||
} | ||
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')); | ||
} | ||
}); | ||
} | ||
@@ -477,3 +527,4 @@ | ||
util.extend(oauthParams, { | ||
display: 'popup' | ||
display: 'popup', | ||
responseMode: 'okta_post_message' | ||
}); | ||
@@ -483,46 +534,87 @@ return getToken(sdk, oauthParams, options); | ||
function getWithRedirect(sdk, oauthOptions, options) { | ||
oauthOptions = util.clone(oauthOptions) || {}; | ||
function prepareOauthParams(sdk, 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 responseType = oauthParams.responseType; | ||
if (typeof responseType === 'string') { | ||
responseType = [responseType]; | ||
} | ||
if (oauthParams.grantType !== 'authorization_code') { | ||
if (responseType.includes('code')) { | ||
return Q.reject(new AuthSdkError('When responseType is "code", grantType should be "authorization_code"')); | ||
} | ||
return Q.resolve(oauthParams); | ||
} | ||
var urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options); | ||
var requestUrl = urls.authorizeUrl + buildAuthorizeParams(oauthParams); | ||
if (!sdk.features.isPKCESupported()) { | ||
return Q.reject(new AuthSdkError('This browser doesn\'t support PKCE')); | ||
} | ||
// Set session cookie to store the oauthParams | ||
cookies.set(config.REDIRECT_OAUTH_PARAMS_COOKIE_NAME, JSON.stringify({ | ||
responseType: oauthParams.responseType, | ||
state: oauthParams.state, | ||
nonce: oauthParams.nonce, | ||
scopes: oauthParams.scopes, | ||
clientId: oauthParams.clientId, | ||
urls: urls, | ||
ignoreSignature: oauthParams.ignoreSignature | ||
})); | ||
if (responseType.length !== 1 || responseType[0] !== 'code') { | ||
return Q.reject(new AuthSdkError('When grantType is "authorization_code", responseType should be "code"')); | ||
} | ||
// Set nonce cookie for servers to validate nonce in id_token | ||
cookies.set(config.REDIRECT_NONCE_COOKIE_NAME, oauthParams.nonce); | ||
return oauthUtil.getWellKnown(sdk, null) | ||
.then(function(res) { | ||
var methods = res['code_challenge_methods_supported'] || []; | ||
if (methods.indexOf(oauthParams.codeChallengeMethod) === -1) { | ||
throw new AuthSdkError('Invalid code_challenge_method'); | ||
} | ||
}) | ||
.then(function() { | ||
// PKCE authorization_code flow | ||
var codeVerifier = pkce.generateVerifier(oauthParams.codeVerifier); | ||
// Set state cookie for servers to validate state | ||
cookies.set(config.REDIRECT_STATE_COOKIE_NAME, oauthParams.state); | ||
// We will need these values after redirect when we call /token | ||
var meta = { | ||
codeVerifier: codeVerifier, | ||
redirectUri: oauthParams.redirectUri | ||
}; | ||
pkce.saveMeta(sdk, meta); | ||
sdk.token.getWithRedirect._setLocation(requestUrl); | ||
return pkce.computeChallenge(codeVerifier); | ||
}) | ||
.then(function(codeChallenge) { | ||
// Clone/copy the params. Set codeChallenge and responseType for authorization_code | ||
var clonedParams = util.clone(oauthParams) || {}; | ||
util.extend(clonedParams, oauthParams, { | ||
codeChallenge: codeChallenge, | ||
}); | ||
return clonedParams; | ||
}); | ||
} | ||
function getWithRedirect(sdk, oauthOptions, options) { | ||
oauthOptions = util.clone(oauthOptions) || {}; | ||
if (!oauthOptions.responseMode) { | ||
oauthOptions.responseMode = 'fragment'; | ||
} | ||
return prepareOauthParams(sdk, oauthOptions) | ||
.then(function(oauthParams) { | ||
var urls = oauthUtil.getOAuthUrls(sdk, oauthParams, options); | ||
var requestUrl = urls.authorizeUrl + buildAuthorizeParams(oauthParams); | ||
// Set session cookie to store the oauthParams | ||
cookies.set(config.REDIRECT_OAUTH_PARAMS_COOKIE_NAME, JSON.stringify({ | ||
responseType: oauthParams.responseType, | ||
state: oauthParams.state, | ||
nonce: oauthParams.nonce, | ||
scopes: oauthParams.scopes, | ||
clientId: oauthParams.clientId, | ||
urls: urls, | ||
ignoreSignature: oauthParams.ignoreSignature | ||
})); | ||
// Set nonce cookie for servers to validate nonce in id_token | ||
cookies.set(config.REDIRECT_NONCE_COOKIE_NAME, oauthParams.nonce); | ||
// Set state cookie for servers to validate state | ||
cookies.set(config.REDIRECT_STATE_COOKIE_NAME, oauthParams.state); | ||
sdk.token.getWithRedirect._setLocation(requestUrl); | ||
}); | ||
} | ||
function renewToken(sdk, token) { | ||
@@ -535,3 +627,5 @@ if (!oauthUtil.isToken(token)) { | ||
var responseType; | ||
if (token.accessToken) { | ||
if (sdk.options.grantType === 'authorization_code') { | ||
responseType = 'code'; | ||
} else if (token.accessToken) { | ||
responseType = 'token'; | ||
@@ -541,2 +635,3 @@ } else { | ||
} | ||
return sdk.token.getWithoutPrompt({ | ||
@@ -640,3 +735,5 @@ responseType: responseType, | ||
getUserInfo: getUserInfo, | ||
verifyToken: verifyToken | ||
verifyToken: verifyToken, | ||
handleOAuthResponse: handleOAuthResponse, | ||
prepareOauthParams: prepareOauthParams | ||
}; |
@@ -143,14 +143,26 @@ /*! | ||
tokenMgmtRef.renewPromise[key] = sdk.token.renew(token) | ||
.then(function(freshToken) { | ||
if (!get(storage, key)) { | ||
// It is possible to enter a state where the tokens have been cleared | ||
// after a renewal request was triggered. To ensure we do not store a | ||
// renewed token, we verify the promise key doesn't exist and return. | ||
return; | ||
.then(function(freshTokens) { | ||
// We may receive more tokens than we requested | ||
var map = {}; | ||
if (freshTokens instanceof Array === false) { | ||
freshTokens = [freshTokens]; | ||
} | ||
add(sdk, tokenMgmtRef, storage, key, freshToken); | ||
tokenMgmtRef.emitter.emit('renewed', key, freshToken, token); | ||
freshTokens.forEach(function(freshToken) { | ||
var inferredKey = freshToken.idToken ? 'idToken' : freshToken.accessToken ? 'accessToken' : key; | ||
map[inferredKey] = freshToken; | ||
var oldToken = get(storage, inferredKey); | ||
if (!oldToken) { | ||
// It is possible to enter a state where the tokens have been cleared | ||
// after a renewal request was triggered. To ensure we do not store a | ||
// renewed token, we verify the promise key doesn't exist and return. | ||
return; | ||
} | ||
add(sdk, tokenMgmtRef, storage, inferredKey, freshToken); | ||
tokenMgmtRef.emitter.emit('renewed', inferredKey, freshToken, oldToken); | ||
}); | ||
// Remove existing promise key | ||
delete tokenMgmtRef.renewPromise[key]; | ||
return freshToken; | ||
return map[key]; // return the specific token requested | ||
}) | ||
@@ -157,0 +169,0 @@ .fail(function(err) { |
@@ -15,2 +15,14 @@ /*! | ||
// converts a string to base64 (url/filename safe variant) | ||
util.stringToBase64Url = function(str) { | ||
var b64 = btoa(str); | ||
return util.base64ToBase64Url(b64); | ||
}; | ||
// converts a standard base64-encoded string to a "url/filename safe" variant | ||
util.base64ToBase64Url = function(b64) { | ||
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); | ||
}; | ||
// converts a "url/filename safe" base64 string to a "standard" base64 string | ||
util.base64UrlToBase64 = function(b64u) { | ||
@@ -17,0 +29,0 @@ return b64u.replace(/-/g, '+').replace(/_/g, '/'); |
{ | ||
"name": "@okta/okta-auth-js", | ||
"description": "The Okta Auth SDK", | ||
"version": "2.5.0", | ||
"version": "2.6.0", | ||
"homepage": "https://github.com/okta/okta-auth-js", | ||
@@ -19,3 +19,4 @@ "license": "Apache-2.0", | ||
"lint:report": "eslint -f checkstyle -o build2/reports/lint/eslint-checkstyle-result.xml .", | ||
"test": "yarn test:browser && yarn test:server", | ||
"test": "yarn test:karma && yarn test:browser && yarn test:server", | ||
"test:karma": "karma start --single-run", | ||
"test:browser": "jest --config ./jest.browser.js", | ||
@@ -25,3 +26,7 @@ "test:server": "jest --config ./jest.server.js", | ||
"build": "node ./writeConfig.js && webpack --config webpack.config.js", | ||
"prepublish": "yarn build" | ||
"prepare": "yarn build", | ||
"install:app": "yarn --cwd test/app install", | ||
"start:app": "yarn --cwd test/app start", | ||
"prestart": "yarn install:app", | ||
"start": "yarn start:app" | ||
}, | ||
@@ -47,2 +52,4 @@ "author": "Okta", | ||
"eslint": "5.6.1", | ||
"istanbul-instrumenter-loader": "^3.0.1", | ||
"jasmine-ajax": "^4.0.0", | ||
"jest": "^23.6.0", | ||
@@ -52,4 +59,11 @@ "jest-junit": "^5.0.0", | ||
"json-loader": "0.5.4", | ||
"karma": "^4.1.0", | ||
"karma-chrome-launcher": "^2.2.0", | ||
"karma-coverage-istanbul-reporter": "^2.0.5", | ||
"karma-jasmine": "^2.0.1", | ||
"karma-jquery": "^0.2.3", | ||
"karma-sourcemap-loader": "^0.3.7", | ||
"karma-webpack": "^3.0.5", | ||
"lodash": "4.17.11", | ||
"webpack": "1.13.0" | ||
"webpack": "^3.0.0" | ||
}, | ||
@@ -66,3 +80,4 @@ "okta-auth-js": { | ||
"TOKEN_STORAGE_NAME": "okta-token-storage", | ||
"CACHE_STORAGE_NAME": "okta-cache-storage" | ||
"CACHE_STORAGE_NAME": "okta-cache-storage", | ||
"PKCE_STORAGE_NAME": "okta-pkce-storage" | ||
}, | ||
@@ -73,5 +88,5 @@ "jest-junit": { | ||
"okta": { | ||
"commitSha": "0e998a8e66fc90a25b97028dcebc275b83b67ccc", | ||
"fullVersion": "2.5.0-20190415215843-0e998a8" | ||
"commitSha": "f5f930605fda763c22ef2bf5c03ba4d83ad4ad56", | ||
"fullVersion": "2.6.0-20190603213603-f5f9306" | ||
} | ||
} |
@@ -165,4 +165,6 @@ [<img src="https://devforum.okta.com/uploads/oktadev/original/1X/bf54a16b5fda189e4ad2706fb57cbb7a1e5b8deb.png" align="right" width="256px"/>](https://devforum.okta.com/) | ||
| `redirectUri` | The url that is redirected to when using `token.getWithRedirect`. This must be pre-registered as part of client registration. If no `redirectUri` is provided, defaults to the current origin. | | ||
| `grantType` | Specify `grantType` for this Application. Supported types are `implicit` and `authorization_code`. Defaults to `implicit` | | ||
| `authorizeUrl` | Specify a custom authorizeUrl to perform the OIDC flow. Defaults to the issuer plus "/v1/authorize". | | ||
| `userinfoUrl` | Specify a custom userinfoUrl. Defaults to the issuer plus "/v1/userinfo". | | ||
| `tokenUrl` | Specify a custom tokenUrl. Defaults to the issuer plus "/v1/token". | | ||
| `ignoreSignature` | ID token signatures are validated by default when `token.getWithoutPrompt`, `token.getWithPopup`, `token.getWithRedirect`, and `token.verify` are called. To disable ID token signature validation for these methods, set this value to `true`. | | ||
@@ -195,2 +197,24 @@ | | This option should be used only for browser support and testing purposes. | | ||
##### PKCE OAuth 2.0 flow | ||
By default the `implicit` OAuth flow will be used. It is widely supported by most browsers. PKCE is a newer flow which is more secure, but does require certain capabilities from the browser. | ||
To use PKCE flow, set `grantType` to `authorization_code` in your config. | ||
```javascript | ||
var config = { | ||
grantType: 'authorization_code', | ||
// other config | ||
issuer: 'https://{yourOktaDomain}/oauth2/default', | ||
}; | ||
var authClient = new OktaAuth(config); | ||
``` | ||
If the user's browser does not support PKCE, an exception will be thrown. You can test if a browser supports PKCE before construction with this static method: | ||
`OktaAuth.features.isPKCESupported()` | ||
### Optional configuration options | ||
@@ -217,3 +241,4 @@ | ||
// }, | ||
// data: postBodyData | ||
// data: postBodyData, | ||
// withCredentials: true|false, | ||
// } | ||
@@ -492,3 +517,3 @@ return Promise.resolve(/* a raw XMLHttpRequest response */); | ||
![State Model Diagram](https://raw.githubusercontent.com/okta/okta.github.io/source/_source/_assets/img/auth-state-model.png "State Model Diagram") | ||
![State Model Diagram](https://developer.okta.com/img/auth-state-model.png "State Model Diagram") | ||
@@ -1374,3 +1399,3 @@ #### Common methods | ||
| `responseMode` | Specify how the authorization response should be returned. You will generally not need to set this unless you want to override the default values for `token.getWithRedirect`. See [Parameter Details](https://developer.okta.com/docs/api/resources/oidc#parameter-details) for a list of available modes. | | ||
| `responseType` | Specify the [response type](https://developer.okta.com/docs/api/resources/oidc#request-parameters) for OIDC authentication. Defaults to `id_token`. | | ||
| `responseType` | Specify the [response type](https://developer.okta.com/docs/api/resources/oidc#request-parameters) for OIDC authentication. The default value is based on the configured `grantType`. If `grantType` is `implicit` (the default setting), `responseType` will have a default value of `id_token`. If `grantType` is `authorization_code`, the default value will be `code`. | | ||
| | Use an array if specifying multiple response types - in this case, the response will contain both an ID Token and an Access Token. `responseType: ['id_token', 'token']` | | ||
@@ -1383,2 +1408,3 @@ | `scopes` | Specify what information to make available in the returned `id_token` or `access_token`. For OIDC, you must include `openid` as one of the scopes. Defaults to `['openid', 'email']`. For a list of available scopes, see [Scopes and Claims](https://developer.okta.com/docs/api/resources/oidc#access-token-scopes-and-claims). | | ||
##### Example | ||
@@ -1454,4 +1480,9 @@ | ||
Parses the access or ID Tokens from the url after a successful authentication redirect. If an ID token is present, it will be [verified and validated](https://github.com/okta/okta-auth-js/blob/master/lib/token.js#L186-L190) before available for use. | ||
Parses the authorization code, access, or ID Tokens from the URL after a successful authentication redirect. | ||
If an authorization code is present, it will be exchanged for token(s) by posting to the `tokenUrl` endpoint. | ||
The ID token will be [verified and validated](https://github.com/okta/okta-auth-js/blob/master/lib/token.js#L186-L190) before available for use. | ||
```javascript | ||
@@ -1728,5 +1759,10 @@ authClient.token.parseFromUrl() | ||
| `yarn build` | Build the SDK with a sourcemap | | ||
| `yarn test` | Run unit tests using Jest | | ||
| `yarn test` | Run unit tests | | ||
| `yarn lint` | Run eslint linting | | ||
| `yarn start` | Start internal test app | | ||
#### Test App | ||
Implements a simple SPA application to demonstrate functionality and provide for manual testing. [See here for more information](test/app/README.md). | ||
## Contributing | ||
@@ -1733,0 +1769,0 @@ |
@@ -16,2 +16,3 @@ /*! | ||
function reqwestRequest(method, url, args) { | ||
// TODO: support content-type and withCredentials | ||
var r = reqwest({ | ||
@@ -22,3 +23,3 @@ url: url, | ||
data: JSON.stringify(args.data), | ||
withCredentials: true | ||
withCredentials: args.withCredentials | ||
}) | ||
@@ -25,0 +26,0 @@ .then(function() { |
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 not supported yet
1283677
3440
1768
16
44