-
Notifications
You must be signed in to change notification settings - Fork 271
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added issuer, authorizeUrl, and userinfoUrl (#47)
Resolves: OKTA-103713
- Loading branch information
1 parent
355acdf
commit e39e6cd
Showing
11 changed files
with
1,479 additions
and
331 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
/* eslint-disable complexity, max-statements */ | ||
var http = require('./http'); | ||
var util = require('./util'); | ||
var AuthSdkError = require('./errors/AuthSdkError'); | ||
|
||
function isToken(obj) { | ||
if (obj && | ||
(obj.accessToken || obj.idToken) && | ||
Array.isArray(obj.scopes)) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
function addListener(eventTarget, name, fn) { | ||
if (eventTarget.addEventListener) { | ||
eventTarget.addEventListener(name, fn); | ||
} else { | ||
eventTarget.attachEvent('on' + name, fn); | ||
} | ||
} | ||
|
||
function removeListener(eventTarget, name, fn) { | ||
if (eventTarget.removeEventListener) { | ||
eventTarget.removeEventListener(name, fn); | ||
} else { | ||
eventTarget.detachEvent('on' + name, fn); | ||
} | ||
} | ||
|
||
function loadFrame(src) { | ||
var iframe = document.createElement('iframe'); | ||
iframe.style.display = 'none'; | ||
iframe.src = src; | ||
|
||
return document.body.appendChild(iframe); | ||
} | ||
|
||
function loadPopup(src, options) { | ||
var title = options.popupTitle || 'External Identity Provider User Authentication'; | ||
var appearance = 'toolbar=no, scrollbars=yes, resizable=yes, ' + | ||
'top=100, left=500, width=600, height=600'; | ||
return window.open(src, title, appearance); | ||
} | ||
|
||
function getWellKnown(sdk) { | ||
// TODO: Use the issuer when known (usually from the id_token) | ||
return http.get(sdk, sdk.options.url + '/.well-known/openid-configuration'); | ||
} | ||
|
||
function validateClaims(sdk, claims, aud, iss) { | ||
if (!claims || !iss || !aud) { | ||
throw new AuthSdkError('The jwt, iss, and aud arguments are all required'); | ||
} | ||
|
||
var now = Math.floor(new Date().getTime()/1000); | ||
|
||
if (claims.iss !== iss) { | ||
throw new AuthSdkError('The issuer [' + claims.iss + '] ' + | ||
'does not match [' + iss + ']'); | ||
} | ||
|
||
if (claims.aud !== aud) { | ||
throw new AuthSdkError('The audience [' + claims.aud + '] ' + | ||
'does not match [' + aud + ']'); | ||
} | ||
|
||
if (claims.iat > claims.exp) { | ||
throw new AuthSdkError('The JWT expired before it was issued'); | ||
} | ||
|
||
if ((now - sdk.options.maxClockSkew) > claims.exp) { | ||
throw new AuthSdkError('The JWT expired and is no longer valid'); | ||
} | ||
|
||
if (claims.iat > (now + sdk.options.maxClockSkew)) { | ||
throw new AuthSdkError('The JWT was issued in the future'); | ||
} | ||
} | ||
|
||
function getOAuthUrls(sdk, oauthParams, options) { | ||
options = options || {}; | ||
|
||
// Get user-supplied arguments | ||
var authorizeUrl = util.removeTrailingSlash(options.authorizeUrl) || sdk.options.authorizeUrl; | ||
var issuer = util.removeTrailingSlash(options.issuer) || sdk.options.issuer; | ||
var userinfoUrl = util.removeTrailingSlash(options.userinfoUrl) || sdk.options.userinfoUrl; | ||
|
||
// 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; | ||
} | ||
|
||
// 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.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'); | ||
} | ||
} | ||
|
||
var sharedResourceServerRegex = new RegExp('^https?://.*?/oauth2/.+'); | ||
|
||
// Default the issuer to our baseUrl | ||
issuer = issuer || sdk.options.url; | ||
|
||
// A shared resource server issuer looks like: | ||
// https://example.okta.com/oauth2/aus8aus76q8iphupD0h7 | ||
if (sharedResourceServerRegex.test(issuer)) { | ||
// A shared resource server authorizeUrl looks like: | ||
// https://example.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/authorize | ||
authorizeUrl = authorizeUrl || issuer + '/v1/authorize'; | ||
// Shared resource server userinfoUrls look like: | ||
// https://example.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/userinfo | ||
userinfoUrl = userinfoUrl || issuer + '/v1/userinfo'; | ||
|
||
// Normally looks like: | ||
// https://example.okta.com | ||
} else { | ||
// Normal authorizeUrls look like: | ||
// https://example.okta.com/oauth2/v1/authorize | ||
authorizeUrl = authorizeUrl || issuer + '/oauth2/v1/authorize'; | ||
// Normal userinfoUrls look like: | ||
// https://example.okta.com/oauth2/v1/userinfo | ||
userinfoUrl = userinfoUrl || issuer + '/oauth2/v1/userinfo'; | ||
} | ||
|
||
return { | ||
issuer: issuer, | ||
authorizeUrl: authorizeUrl, | ||
userinfoUrl: userinfoUrl | ||
}; | ||
} | ||
|
||
function hashToObject(hash) { | ||
// Predefine regexs for parsing hash | ||
var plus2space = /\+/g; | ||
var paramSplit = /([^&=]+)=?([^&]*)/g; | ||
|
||
// Remove the leading hash | ||
var fragment = hash.substring(1); | ||
|
||
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; } | ||
|
||
var key = param[1]; | ||
var value = param[2]; | ||
|
||
// id_token should remain base64url encoded | ||
if (key === 'id_token' || key === 'access_token' || key === 'code') { | ||
obj[key] = value; | ||
} else { | ||
obj[key] = decodeURIComponent(value.replace(plus2space, ' ')); | ||
} | ||
} | ||
return obj; | ||
} | ||
|
||
module.exports = { | ||
getWellKnown: getWellKnown, | ||
validateClaims: validateClaims, | ||
getOAuthUrls: getOAuthUrls, | ||
loadFrame: loadFrame, | ||
loadPopup: loadPopup, | ||
hashToObject: hashToObject, | ||
isToken: isToken, | ||
addListener: addListener, | ||
removeListener: removeListener | ||
}; |
Oops, something went wrong.