From af21c3bffd3a0f0360191aeebba27124af0ca8fc Mon Sep 17 00:00:00 2001 From: Len Boyette Date: Thu, 20 Oct 2016 15:55:20 -0700 Subject: [PATCH] Added caching to well-known configuration (#48) Resolves: OKTA-100724 --- lib/TokenManager.js | 23 +++++----- lib/http.js | 43 +++++++++++++----- lib/oauthUtil.js | 4 +- lib/session.js | 3 +- lib/storageBuilder.js | 42 +++++++++++++++++ lib/token.js | 1 - lib/tokenStorageBuilder.js | 36 --------------- lib/tx.js | 4 +- package.json | 7 ++- test/spec/oauthUtil.js | 93 ++++++++++++++++++++++++++++++++++++++ test/spec/tokenManager.js | 4 +- test/util/util.js | 56 ++++++++++++----------- 12 files changed, 223 insertions(+), 93 deletions(-) create mode 100644 lib/storageBuilder.js delete mode 100644 lib/tokenStorageBuilder.js diff --git a/lib/TokenManager.js b/lib/TokenManager.js index 646a2ffee..e26523b6b 100644 --- a/lib/TokenManager.js +++ b/lib/TokenManager.js @@ -1,9 +1,10 @@ var util = require('./util'); var AuthSdkError = require('./errors/AuthSdkError'); var cookies = require('./cookies'); -var tokenStorageBuilder = require('./tokenStorageBuilder'); +var storageBuilder = require('./storageBuilder'); var Q = require('q'); var Emitter = require('tiny-emitter'); +var config = require('./config'); // Provides webStorage-like interface for cookies var cookieStorage = { @@ -58,7 +59,7 @@ function setRefreshTimeout(sdk, tokenMgmtRef, storage, key, token) { function setRefreshTimeoutAll(sdk, tokenMgmtRef, storage) { try { - var tokenStorage = storage.getTokenStorage(); + var tokenStorage = storage.getStorage(); } catch(e) { // Any errors thrown on instantiation will not be caught, // because there are no listeners yet @@ -76,7 +77,7 @@ function setRefreshTimeoutAll(sdk, tokenMgmtRef, storage) { } function add(sdk, tokenMgmtRef, storage, key, token) { - var tokenStorage = storage.getTokenStorage(); + var tokenStorage = storage.getStorage(); if (!util.isObject(token) || !token.scopes || (!token.expiresAt && token.expiresAt !== 0) || @@ -84,12 +85,12 @@ function add(sdk, tokenMgmtRef, storage, key, token) { throw new AuthSdkError('Token must be an Object with scopes, expiresAt, and an idToken or accessToken properties'); } tokenStorage[key] = token; - storage.setTokenStorage(tokenStorage); + storage.setStorage(tokenStorage); setRefreshTimeout(sdk, tokenMgmtRef, storage, key, token); } function get(storage, key) { - var tokenStorage = storage.getTokenStorage(); + var tokenStorage = storage.getStorage(); return tokenStorage[key]; } @@ -98,9 +99,9 @@ function remove(tokenMgmtRef, storage, key) { clearRefreshTimeout(tokenMgmtRef, key); // Remove it from storage - var tokenStorage = storage.getTokenStorage(); + var tokenStorage = storage.getStorage(); delete tokenStorage[key]; - storage.setTokenStorage(tokenStorage); + storage.setStorage(tokenStorage); } function refresh(sdk, tokenMgmtRef, storage, key) { @@ -133,7 +134,7 @@ function refresh(sdk, tokenMgmtRef, storage, key) { function clear(tokenMgmtRef, storage) { clearRefreshTimeoutAll(tokenMgmtRef); - storage.clearTokenStorage(); + storage.clearStorage(); } function TokenManager(sdk, options) { @@ -146,13 +147,13 @@ function TokenManager(sdk, options) { var storage; switch(options.storage) { case 'localStorage': - storage = tokenStorageBuilder(localStorage); + storage = storageBuilder(localStorage, config.TOKEN_STORAGE_NAME); break; case 'sessionStorage': - storage = tokenStorageBuilder(sessionStorage); + storage = storageBuilder(sessionStorage, config.TOKEN_STORAGE_NAME); break; case 'cookie': - storage = tokenStorageBuilder(cookieStorage); + storage = storageBuilder(cookieStorage, config.TOKEN_STORAGE_NAME); break; default: throw new AuthSdkError('Unrecognized storage option'); diff --git a/lib/http.js b/lib/http.js index 4e397c075..f09c20d1c 100644 --- a/lib/http.js +++ b/lib/http.js @@ -4,15 +4,26 @@ var cookies = require('./cookies'); var Q = require('q'); var AuthApiError = require('./errors/AuthApiError'); var config = require('./config'); +var storageBuilder = require('./storageBuilder'); + +var httpCache = storageBuilder(localStorage, config.CACHE_STORAGE_NAME); function httpRequest(sdk, options) { options = options || {}; var url = options.url, method = options.method, args = options.args, - dontSaveResponse = options.dontSaveResponse, + saveAuthnState = options.saveAuthnState, accessToken = options.accessToken; + if (options.cacheResponse) { + var cacheContents = httpCache.getStorage(); + var cachedResponse = cacheContents[url]; + if (cachedResponse && Date.now()/1000 < cachedResponse.expiresAt) { + return Q.resolve(cachedResponse.response); + } + } + var headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', @@ -37,7 +48,7 @@ function httpRequest(sdk, options) { res = JSON.parse(res); } - if (!dontSaveResponse) { + if (saveAuthnState) { if (!res.stateToken) { cookies.deleteCookie(config.STATE_TOKEN_COOKIE_NAME); } @@ -47,6 +58,13 @@ function httpRequest(sdk, options) { cookies.setCookie(config.STATE_TOKEN_COOKIE_NAME, res.stateToken, res.expiresAt); } + if (res && options.cacheResponse) { + httpCache.updateStorage(url, { + expiresAt: Math.floor(Date.now()/1000) + config.DEFAULT_CACHE_DURATION, + response: res + }); + } + return res; }) .fail(function(resp) { @@ -79,23 +97,26 @@ function httpRequest(sdk, options) { }); } -function get(sdk, url, saveResponse) { +function get(sdk, url, options) { url = util.isAbsoluteUrl(url) ? url : sdk.options.url + url; - return httpRequest(sdk, { + var getOptions = { url: url, - method: 'GET', - dontSaveResponse: !saveResponse - }); + method: 'GET' + }; + util.extend(getOptions, options); + return httpRequest(sdk, getOptions); } -function post(sdk, url, args, dontSaveResponse) { +function post(sdk, url, args, options) { url = util.isAbsoluteUrl(url) ? url : sdk.options.url + url; - return httpRequest(sdk, { + var postOptions = { url: url, method: 'POST', args: args, - dontSaveResponse: dontSaveResponse - }); + saveAuthnState: true + }; + util.extend(postOptions, options); + return httpRequest(sdk, postOptions); } module.exports = { diff --git a/lib/oauthUtil.js b/lib/oauthUtil.js index fc895d295..ac970c42c 100644 --- a/lib/oauthUtil.js +++ b/lib/oauthUtil.js @@ -45,7 +45,9 @@ function loadPopup(src, options) { 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'); + return http.get(sdk, sdk.options.url + '/.well-known/openid-configuration', { + cacheResponse: true + }); } function validateClaims(sdk, claims, aud, iss) { diff --git a/lib/session.js b/lib/session.js index 6257a63b5..ff551fb3b 100644 --- a/lib/session.js +++ b/lib/session.js @@ -38,8 +38,7 @@ function getSession(sdk) { function closeSession(sdk) { return http.httpRequest(sdk, { url: sdk.options.url + '/api/v1/sessions/me', - method: 'DELETE', - dontSaveResponse: true + method: 'DELETE' }); } diff --git a/lib/storageBuilder.js b/lib/storageBuilder.js new file mode 100644 index 000000000..fe5c85368 --- /dev/null +++ b/lib/storageBuilder.js @@ -0,0 +1,42 @@ +var AuthSdkError = require('./errors/AuthSdkError'); + +// storage must have getItem and setItem +function storageBuilder(webstorage, storageName) { + function getStorage() { + var storageString = webstorage.getItem(storageName); + storageString = storageString || '{}'; + try { + return JSON.parse(storageString); + } catch(e) { + throw new AuthSdkError('Unable to parse storage string: ' + storageName); + } + } + + function setStorage(storage) { + try { + var storageString = JSON.stringify(storage); + webstorage.setItem(storageName, storageString); + } catch(e) { + throw new AuthSdkError('Unable to set storage: ' + storageName); + } + } + + function clearStorage() { + setStorage({}); + } + + function updateStorage(key, value) { + var storage = getStorage(); + storage[key] = value; + setStorage(storage); + } + + return { + getStorage: getStorage, + setStorage: setStorage, + clearStorage: clearStorage, + updateStorage: updateStorage + }; +} + +module.exports = storageBuilder; diff --git a/lib/token.js b/lib/token.js index 02e09139d..98e3009da 100644 --- a/lib/token.js +++ b/lib/token.js @@ -590,7 +590,6 @@ function getUserInfo(sdk, accessTokenObject) { return http.httpRequest(sdk, { url: accessTokenObject.userinfoUrl, method: 'GET', - dontSaveResponse: true, accessToken: accessTokenObject.accessToken }) .fail(function(err) { diff --git a/lib/tokenStorageBuilder.js b/lib/tokenStorageBuilder.js deleted file mode 100644 index 7612b20ae..000000000 --- a/lib/tokenStorageBuilder.js +++ /dev/null @@ -1,36 +0,0 @@ -var AuthSdkError = require('./errors/AuthSdkError'); -var config = require('./config'); - -// storage must have getItem and setItem -function tokenStorageBuilder(storage) { - function getTokenStorage() { - var tokenStorageString = storage.getItem(config.TOKEN_STORAGE_NAME); - tokenStorageString = tokenStorageString || '{}'; - try { - return JSON.parse(tokenStorageString); - } catch(e) { - throw new AuthSdkError('Unable to parse token storage string'); - } - } - - function setTokenStorage(tokenStorage) { - try { - var tokenStorageString = JSON.stringify(tokenStorage); - storage.setItem(config.TOKEN_STORAGE_NAME, tokenStorageString); - } catch(e) { - throw new AuthSdkError('Unable to set token storage string'); - } - } - - function clearTokenStorage() { - setTokenStorage({}); - } - - return { - getTokenStorage: getTokenStorage, - setTokenStorage: setTokenStorage, - clearTokenStorage: clearTokenStorage - }; -} - -module.exports = tokenStorageBuilder; diff --git a/lib/tx.js b/lib/tx.js index 63ccbcb2b..69c992ab7 100644 --- a/lib/tx.js +++ b/lib/tx.js @@ -78,7 +78,9 @@ function getPollFn(sdk, res, ref) { if (rememberDevice) { href += '?rememberDevice=true'; } - return http.post(sdk, href, getStateToken(res), true, true); + return http.post(sdk, href, getStateToken(res), { + saveAuthnState: false + }); } ref.isPolling = true; diff --git a/package.json b/package.json index 26af3baee..a34227c42 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,10 @@ "STATE_TOKEN_COOKIE_NAME": "oktaStateToken", "DEFAULT_POLLING_DELAY": 500, "DEFAULT_MAX_CLOCK_SKEW": 300, + "DEFAULT_CACHE_DURATION": 86400, + "FRAME_ID": "okta-oauth-helper-frame", "REDIRECT_OAUTH_PARAMS_COOKIE_NAME": "okta-oauth-redirect-params", - "TOKEN_STORAGE_NAME": "okta-token-storage" + "TOKEN_STORAGE_NAME": "okta-token-storage", + "CACHE_STORAGE_NAME": "okta-cache-storage" } -} \ No newline at end of file +} diff --git a/test/spec/oauthUtil.js b/test/spec/oauthUtil.js index 212a4eb25..a8a843e79 100644 --- a/test/spec/oauthUtil.js +++ b/test/spec/oauthUtil.js @@ -1,6 +1,99 @@ define(function(require) { var OktaAuth = require('OktaAuth'); var oauthUtil = require('../../lib/oauthUtil'); + var util = require('../util/util'); + var wellKnown = require('../xhr/well-known'); + + describe('getWellKnown', function() { + util.itMakesCorrectRequestResponse({ + title: 'caches response and uses cache on subsequent requests', + setup: { + calls: [ + { + request: { + method: 'get', + uri: '/.well-known/openid-configuration' + }, + response: 'well-known' + } + ], + time: 1449699929 + }, + execute: function(test) { + localStorage.clear(); + return oauthUtil.getWellKnown(test.oa) + .then(function() { + return oauthUtil.getWellKnown(test.oa); + }); + }, + expectations: function() { + var cache = localStorage.getItem('okta-cache-storage'); + expect(cache).toEqual(JSON.stringify({ + 'https://auth-js-test.okta.com/.well-known/openid-configuration': { + expiresAt: 1449786329, + response: wellKnown.response + } + })); + } + }); + util.itMakesCorrectRequestResponse({ + title: 'uses cached response', + setup: { + time: 1449699929 + }, + execute: function(test) { + localStorage.setItem('okta-cache-storage', JSON.stringify({ + 'https://auth-js-test.okta.com/.well-known/openid-configuration': { + expiresAt: 1449786329, + response: wellKnown.response + } + })); + return oauthUtil.getWellKnown(test.oa); + }, + expectations: function() { + var cache = localStorage.getItem('okta-cache-storage'); + expect(cache).toEqual(JSON.stringify({ + 'https://auth-js-test.okta.com/.well-known/openid-configuration': { + expiresAt: 1449786329, + response: wellKnown.response + } + })); + } + }); + util.itMakesCorrectRequestResponse({ + title: 'doesn\'t use cached response if past cache expiration', + setup: { + calls: [ + { + request: { + method: 'get', + uri: '/.well-known/openid-configuration' + }, + response: 'well-known' + } + ], + time: 1450000000 + }, + execute: function(test) { + localStorage.setItem('okta-cache-storage', JSON.stringify({ + 'https://auth-js-test.okta.com/.well-known/openid-configuration': { + expiresAt: 1449786329, + response: wellKnown.response + } + })); + return oauthUtil.getWellKnown(test.oa); + }, + expectations: function() { + var cache = localStorage.getItem('okta-cache-storage'); + expect(cache).toEqual(JSON.stringify({ + 'https://auth-js-test.okta.com/.well-known/openid-configuration': { + expiresAt: 1450086400, + response: wellKnown.response + } + })); + } + }); + }); describe('getOAuthUrls', function() { function setupOAuthUrls(options) { diff --git a/test/spec/tokenManager.js b/test/spec/tokenManager.js index e712f966c..9b0690936 100644 --- a/test/spec/tokenManager.js +++ b/test/spec/tokenManager.js @@ -184,9 +184,9 @@ define(function(require) { .fail(function(err) { util.expectErrorToEqual(err, { name: 'AuthSdkError', - message: 'Unable to parse token storage string', + message: 'Unable to parse storage string: okta-token-storage', errorCode: 'INTERNAL', - errorSummary: 'Unable to parse token storage string', + errorSummary: 'Unable to parse storage string: okta-token-storage', errorLink: 'INTERNAL', errorId: 'INTERNAL', errorCauses: [] diff --git a/test/util/util.js b/test/util/util.js index f4aa63536..f2bf31677 100644 --- a/test/util/util.js +++ b/test/util/util.js @@ -7,6 +7,30 @@ define(function(require) { OktaAuth = require('OktaAuth'), cookies = require('../../lib/cookies'); + + var util = {}; + + util.warpToDistantFuture = function () { + jasmine.clock().mockDate(new Date(9999999999999)); + }; + + util.warpToDistantPast = function () { + jasmine.clock().mockDate(new Date(0)); + }; + + util.warpToUnixTime = function (unixTime) { + jasmine.clock().mockDate(new Date(unixTime * 1000)); + }; + + util.returnToPresent = function () { + jasmine.clock().mockDate(new Date()); + }; + + util.warpByTicksToUnixTime = function (unixTime) { + var ticks = (unixTime * 1000) - Date.now(); + jasmine.clock().tick(ticks); + }; + function generateXHRPair(request, response, uri) { return Q.Promise(function(resolve) { @@ -114,6 +138,10 @@ define(function(require) { return new Q() .then(function() { + if (options.time) { + util.warpToUnixTime(options.time); + } + // 1. Setup ajax mock if (options.calls) { @@ -187,8 +215,6 @@ define(function(require) { }); } - var util = {}; - util.itMakesCorrectRequestResponse = function (options) { var fn = options.only ? it.only : it, title = options.title || 'makes correct request and returns response'; @@ -203,11 +229,10 @@ define(function(require) { } if (options.expectations) { options.expectations(test, res); - test.ajaxMock.done(); - } else { + } else if (test.trans) { expect(test.trans.data).toEqual(test.responseBody); - test.ajaxMock.done(); } + test.ajaxMock.done(); done(); }); }); @@ -270,27 +295,6 @@ define(function(require) { }); }; - util.warpToDistantFuture = function () { - jasmine.clock().mockDate(new Date(9999999999999)); - }; - - util.warpToDistantPast = function () { - jasmine.clock().mockDate(new Date(0)); - }; - - util.warpToUnixTime = function (unixTime) { - jasmine.clock().mockDate(new Date(unixTime * 1000)); - }; - - util.returnToPresent = function () { - jasmine.clock().mockDate(new Date()); - }; - - util.warpByTicksToUnixTime = function (unixTime) { - var ticks = (unixTime * 1000) - Date.now(); - jasmine.clock().tick(ticks); - }; - util.parseUri = function (uri) { var split = uri.split('?'); return {