oauthUtil.js 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. /* eslint-disable complexity, max-statements */
  2. var http = require('./http');
  3. var util = require('./util');
  4. var storageUtil = require('./storageUtil');
  5. var AuthSdkError = require('./errors/AuthSdkError');
  6. var httpCache = storageUtil.getHttpCache();
  7. function isToken(obj) {
  8. if (obj &&
  9. (obj.accessToken || obj.idToken) &&
  10. Array.isArray(obj.scopes)) {
  11. return true;
  12. }
  13. return false;
  14. }
  15. function addListener(eventTarget, name, fn) {
  16. if (eventTarget.addEventListener) {
  17. eventTarget.addEventListener(name, fn);
  18. } else {
  19. eventTarget.attachEvent('on' + name, fn);
  20. }
  21. }
  22. function removeListener(eventTarget, name, fn) {
  23. if (eventTarget.removeEventListener) {
  24. eventTarget.removeEventListener(name, fn);
  25. } else {
  26. eventTarget.detachEvent('on' + name, fn);
  27. }
  28. }
  29. function loadFrame(src) {
  30. var iframe = document.createElement('iframe');
  31. iframe.style.display = 'none';
  32. iframe.src = src;
  33. return document.body.appendChild(iframe);
  34. }
  35. function loadPopup(src, options) {
  36. var title = options.popupTitle || 'External Identity Provider User Authentication';
  37. var appearance = 'toolbar=no, scrollbars=yes, resizable=yes, ' +
  38. 'top=100, left=500, width=600, height=600';
  39. if (util.isIE11OrLess()) {
  40. // IE<=11 doesn't fully support postMessage at time of writting.
  41. // the following simple solution happened to solve the issue
  42. // without adding another proxy layer which makes flow more complecated.
  43. var winEl = window.open('/', title, appearance);
  44. winEl.location.href = src;
  45. return winEl;
  46. } else {
  47. return window.open(src, title, appearance);
  48. }
  49. }
  50. function getWellKnown(sdk, issuer) {
  51. return http.get(sdk, (issuer || sdk.options.url) + '/.well-known/openid-configuration', {
  52. cacheResponse: true
  53. });
  54. }
  55. function getKey(sdk, issuer, kid) {
  56. return getWellKnown(sdk, issuer)
  57. .then(function(wellKnown) {
  58. var jwksUri = wellKnown['jwks_uri'];
  59. // Check our kid against the cached version (if it exists and isn't expired)
  60. var cacheContents = httpCache.getStorage();
  61. var cachedResponse = cacheContents[jwksUri];
  62. if (cachedResponse && Date.now()/1000 < cachedResponse.expiresAt) {
  63. var cachedKey = util.find(cachedResponse.response.keys, {
  64. kid: kid
  65. });
  66. if (cachedKey) {
  67. return cachedKey;
  68. }
  69. }
  70. // Remove cache for the key
  71. httpCache.clearStorage(jwksUri);
  72. // Pull the latest keys if the key wasn't in the cache
  73. return http.get(sdk, jwksUri, {
  74. cacheResponse: true
  75. })
  76. .then(function(res) {
  77. var key = util.find(res.keys, {
  78. kid: kid
  79. });
  80. if (key) {
  81. return key;
  82. }
  83. throw new AuthSdkError('The key id, ' + kid + ', was not found in the server\'s keys');
  84. });
  85. });
  86. }
  87. function validateClaims(sdk, claims, aud, iss, nonce) {
  88. if (!claims || !iss || !aud) {
  89. throw new AuthSdkError('The jwt, iss, and aud arguments are all required');
  90. }
  91. if (nonce && claims.nonce !== nonce) {
  92. throw new AuthSdkError('OAuth flow response nonce doesn\'t match request nonce');
  93. }
  94. var now = Math.floor(new Date().getTime()/1000);
  95. if (claims.iss !== iss) {
  96. throw new AuthSdkError('The issuer [' + claims.iss + '] ' +
  97. 'does not match [' + iss + ']');
  98. }
  99. if (claims.aud !== aud) {
  100. throw new AuthSdkError('The audience [' + claims.aud + '] ' +
  101. 'does not match [' + aud + ']');
  102. }
  103. if (claims.iat > claims.exp) {
  104. throw new AuthSdkError('The JWT expired before it was issued');
  105. }
  106. if ((now - sdk.options.maxClockSkew) > claims.exp) {
  107. throw new AuthSdkError('The JWT expired and is no longer valid');
  108. }
  109. if (claims.iat > (now + sdk.options.maxClockSkew)) {
  110. throw new AuthSdkError('The JWT was issued in the future');
  111. }
  112. }
  113. function getOAuthUrls(sdk, oauthParams, options) {
  114. options = options || {};
  115. // Get user-supplied arguments
  116. var authorizeUrl = util.removeTrailingSlash(options.authorizeUrl) || sdk.options.authorizeUrl;
  117. var issuer = util.removeTrailingSlash(options.issuer) || sdk.options.issuer;
  118. var userinfoUrl = util.removeTrailingSlash(options.userinfoUrl) || sdk.options.userinfoUrl;
  119. // If an issuer exists but it's not a url, assume it's an authServerId
  120. if (issuer && !(/^https?:/.test(issuer))) {
  121. // Make it a url
  122. issuer = sdk.options.url + '/oauth2/' + issuer;
  123. }
  124. // If an authorizeUrl is supplied without an issuer, and an id_token is requested
  125. if (!issuer && authorizeUrl &&
  126. oauthParams.responseType.indexOf('id_token') !== -1) {
  127. // The issuer is ambiguous, so we won't be able to validate the id_token jwt
  128. throw new AuthSdkError('Cannot request idToken with an authorizeUrl without an issuer');
  129. }
  130. // If a token is requested without an issuer
  131. if (!issuer && oauthParams.responseType.indexOf('token') !== -1) {
  132. // If an authorizeUrl is supplied without a userinfoUrl
  133. if (authorizeUrl && !userinfoUrl) {
  134. // The userinfoUrl is ambiguous, so we won't be able to call getUserInfo
  135. throw new AuthSdkError('Cannot request accessToken with an authorizeUrl without an issuer or userinfoUrl');
  136. }
  137. // If a userinfoUrl is supplied without a authorizeUrl
  138. if (userinfoUrl && !authorizeUrl) {
  139. // The authorizeUrl is ambiguous, so we won't be able to call the authorize endpoint
  140. throw new AuthSdkError('Cannot request token with an userinfoUrl without an issuer or authorizeUrl');
  141. }
  142. }
  143. var sharedResourceServerRegex = new RegExp('^https?://.*?/oauth2/.+');
  144. // Default the issuer to our baseUrl
  145. issuer = issuer || sdk.options.url;
  146. // A shared resource server issuer looks like:
  147. // https://example.okta.com/oauth2/aus8aus76q8iphupD0h7
  148. if (sharedResourceServerRegex.test(issuer)) {
  149. // A shared resource server authorizeUrl looks like:
  150. // https://example.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/authorize
  151. authorizeUrl = authorizeUrl || issuer + '/v1/authorize';
  152. // Shared resource server userinfoUrls look like:
  153. // https://example.okta.com/oauth2/aus8aus76q8iphupD0h7/v1/userinfo
  154. userinfoUrl = userinfoUrl || issuer + '/v1/userinfo';
  155. // Normally looks like:
  156. // https://example.okta.com
  157. } else {
  158. // Normal authorizeUrls look like:
  159. // https://example.okta.com/oauth2/v1/authorize
  160. authorizeUrl = authorizeUrl || issuer + '/oauth2/v1/authorize';
  161. // Normal userinfoUrls look like:
  162. // https://example.okta.com/oauth2/v1/userinfo
  163. userinfoUrl = userinfoUrl || issuer + '/oauth2/v1/userinfo';
  164. }
  165. return {
  166. issuer: issuer,
  167. authorizeUrl: authorizeUrl,
  168. userinfoUrl: userinfoUrl
  169. };
  170. }
  171. function hashToObject(hash) {
  172. // Predefine regexs for parsing hash
  173. var plus2space = /\+/g;
  174. var paramSplit = /([^&=]+)=?([^&]*)/g;
  175. // Remove the leading hash
  176. var fragment = hash.substring(1);
  177. var obj = {};
  178. // Loop until we have no more params
  179. var param;
  180. while (true) { // eslint-disable-line no-constant-condition
  181. param = paramSplit.exec(fragment);
  182. if (!param) { break; }
  183. var key = param[1];
  184. var value = param[2];
  185. // id_token should remain base64url encoded
  186. if (key === 'id_token' || key === 'access_token' || key === 'code') {
  187. obj[key] = value;
  188. } else {
  189. obj[key] = decodeURIComponent(value.replace(plus2space, ' '));
  190. }
  191. }
  192. return obj;
  193. }
  194. module.exports = {
  195. getWellKnown: getWellKnown,
  196. getKey: getKey,
  197. validateClaims: validateClaims,
  198. getOAuthUrls: getOAuthUrls,
  199. loadFrame: loadFrame,
  200. loadPopup: loadPopup,
  201. hashToObject: hashToObject,
  202. isToken: isToken,
  203. addListener: addListener,
  204. removeListener: removeListener
  205. };