From fe5c376457c045b38a00fffe139c4de03a78b812 Mon Sep 17 00:00:00 2001 From: lboyette-okta Date: Tue, 27 Sep 2016 10:34:00 -0700 Subject: [PATCH 1/3] Added caching to well-known configuration Resolves: OKTA-100724 --- lib/TokenManager.js | 23 ++++++++-------- lib/http.js | 45 ++++++++++++++++++++++-------- lib/oauthUtil.js | 5 +++- lib/session.js | 3 +- lib/storageBuilder.js | 35 ++++++++++++++++++++++++ lib/token.js | 1 - lib/tokenStorageBuilder.js | 36 ------------------------ lib/tx.js | 4 ++- package.json | 7 +++-- test/spec/oauthUtil.js | 48 ++++++++++++++++++++++++++++++++ test/spec/tokenManager.js | 4 +-- test/util/util.js | 56 ++++++++++++++++++++------------------ 12 files changed, 174 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..7c9694eb6 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, + cacheState = options.cacheState, accessToken = options.accessToken; + if (options.responseCacheDuration) { + 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 (cacheState) { if (!res.stateToken) { cookies.deleteCookie(config.STATE_TOKEN_COOKIE_NAME); } @@ -47,6 +58,15 @@ function httpRequest(sdk, options) { cookies.setCookie(config.STATE_TOKEN_COOKIE_NAME, res.stateToken, res.expiresAt); } + if (res && options.responseCacheDuration) { + var cache = httpCache.getStorage(); + cache[url] = { + expiresAt: Math.floor(Date.now()/1000) + options.responseCacheDuration, + response: res + }; + httpCache.setStorage(cache); + } + return res; }) .fail(function(resp) { @@ -79,23 +99,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 - }); + cacheState: true + }; + util.extend(postOptions, options); + return httpRequest(sdk, postOptions); } module.exports = { diff --git a/lib/oauthUtil.js b/lib/oauthUtil.js index fc895d295..c92fc9ed7 100644 --- a/lib/oauthUtil.js +++ b/lib/oauthUtil.js @@ -2,6 +2,7 @@ var http = require('./http'); var util = require('./util'); var AuthSdkError = require('./errors/AuthSdkError'); +var config = require('./config'); function isToken(obj) { if (obj && @@ -45,7 +46,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', { + responseCacheDuration: config.DEFAULT_CACHE_DURATION + }); } 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..d310b06e4 --- /dev/null +++ b/lib/storageBuilder.js @@ -0,0 +1,35 @@ +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({}); + } + + return { + getStorage: getStorage, + setStorage: setStorage, + clearStorage: clearStorage + }; +} + +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..beea0eb30 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), { + cacheState: 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..cff20c77e 100644 --- a/test/spec/oauthUtil.js +++ b/test/spec/oauthUtil.js @@ -1,6 +1,54 @@ 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', + 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); + }, + 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); + } + }); + }); 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 { From 762c7f8b3f3966c4aa3c090c006248a2e83ba7e0 Mon Sep 17 00:00:00 2001 From: lboyette-okta Date: Wed, 19 Oct 2016 17:26:23 -0700 Subject: [PATCH 2/3] Addressed comments --- lib/http.js | 18 +++++++--------- lib/oauthUtil.js | 3 +-- lib/storageBuilder.js | 9 +++++++- lib/tx.js | 2 +- test/spec/oauthUtil.js | 49 ++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 65 insertions(+), 16 deletions(-) diff --git a/lib/http.js b/lib/http.js index 7c9694eb6..c294599c9 100644 --- a/lib/http.js +++ b/lib/http.js @@ -13,10 +13,10 @@ function httpRequest(sdk, options) { var url = options.url, method = options.method, args = options.args, - cacheState = options.cacheState, + cacheAuthnState = options.cacheAuthnState, accessToken = options.accessToken; - if (options.responseCacheDuration) { + if (options.cacheResponse) { var cacheContents = httpCache.getStorage(); var cachedResponse = cacheContents[url]; if (cachedResponse && Date.now()/1000 < cachedResponse.expiresAt) { @@ -48,7 +48,7 @@ function httpRequest(sdk, options) { res = JSON.parse(res); } - if (cacheState) { + if (cacheAuthnState) { if (!res.stateToken) { cookies.deleteCookie(config.STATE_TOKEN_COOKIE_NAME); } @@ -58,13 +58,11 @@ function httpRequest(sdk, options) { cookies.setCookie(config.STATE_TOKEN_COOKIE_NAME, res.stateToken, res.expiresAt); } - if (res && options.responseCacheDuration) { - var cache = httpCache.getStorage(); - cache[url] = { - expiresAt: Math.floor(Date.now()/1000) + options.responseCacheDuration, + if (res && options.cacheResponse) { + httpCache.updateStorage(url, { + expiresAt: Math.floor(Date.now()/1000) + config.DEFAULT_CACHE_DURATION, response: res - }; - httpCache.setStorage(cache); + }); } return res; @@ -115,7 +113,7 @@ function post(sdk, url, args, options) { url: url, method: 'POST', args: args, - cacheState: true + cacheAuthnState: true }; util.extend(postOptions, options); return httpRequest(sdk, postOptions); diff --git a/lib/oauthUtil.js b/lib/oauthUtil.js index c92fc9ed7..ac970c42c 100644 --- a/lib/oauthUtil.js +++ b/lib/oauthUtil.js @@ -2,7 +2,6 @@ var http = require('./http'); var util = require('./util'); var AuthSdkError = require('./errors/AuthSdkError'); -var config = require('./config'); function isToken(obj) { if (obj && @@ -47,7 +46,7 @@ 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', { - responseCacheDuration: config.DEFAULT_CACHE_DURATION + cacheResponse: true }); } diff --git a/lib/storageBuilder.js b/lib/storageBuilder.js index d310b06e4..fe5c85368 100644 --- a/lib/storageBuilder.js +++ b/lib/storageBuilder.js @@ -25,10 +25,17 @@ function storageBuilder(webstorage, storageName) { setStorage({}); } + function updateStorage(key, value) { + var storage = getStorage(); + storage[key] = value; + setStorage(storage); + } + return { getStorage: getStorage, setStorage: setStorage, - clearStorage: clearStorage + clearStorage: clearStorage, + updateStorage: updateStorage }; } diff --git a/lib/tx.js b/lib/tx.js index beea0eb30..be949e1b7 100644 --- a/lib/tx.js +++ b/lib/tx.js @@ -79,7 +79,7 @@ function getPollFn(sdk, res, ref) { href += '?rememberDevice=true'; } return http.post(sdk, href, getStateToken(res), { - cacheState: false + cacheAuthnState: false }); } diff --git a/test/spec/oauthUtil.js b/test/spec/oauthUtil.js index cff20c77e..a8a843e79 100644 --- a/test/spec/oauthUtil.js +++ b/test/spec/oauthUtil.js @@ -6,7 +6,7 @@ define(function(require) { describe('getWellKnown', function() { util.itMakesCorrectRequestResponse({ - title: 'caches response', + title: 'caches response and uses cache on subsequent requests', setup: { calls: [ { @@ -21,7 +21,10 @@ define(function(require) { }, execute: function(test) { localStorage.clear(); - return oauthUtil.getWellKnown(test.oa); + return oauthUtil.getWellKnown(test.oa) + .then(function() { + return oauthUtil.getWellKnown(test.oa); + }); }, expectations: function() { var cache = localStorage.getItem('okta-cache-storage'); @@ -46,6 +49,48 @@ define(function(require) { } })); 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 + } + })); } }); }); From 45da199cad590acebe5680d4ec1a968ba4e8e300 Mon Sep 17 00:00:00 2001 From: lboyette-okta Date: Thu, 20 Oct 2016 14:46:03 -0700 Subject: [PATCH 3/3] cacheAuthnState => saveAuthnState --- lib/http.js | 6 +++--- lib/tx.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/http.js b/lib/http.js index c294599c9..f09c20d1c 100644 --- a/lib/http.js +++ b/lib/http.js @@ -13,7 +13,7 @@ function httpRequest(sdk, options) { var url = options.url, method = options.method, args = options.args, - cacheAuthnState = options.cacheAuthnState, + saveAuthnState = options.saveAuthnState, accessToken = options.accessToken; if (options.cacheResponse) { @@ -48,7 +48,7 @@ function httpRequest(sdk, options) { res = JSON.parse(res); } - if (cacheAuthnState) { + if (saveAuthnState) { if (!res.stateToken) { cookies.deleteCookie(config.STATE_TOKEN_COOKIE_NAME); } @@ -113,7 +113,7 @@ function post(sdk, url, args, options) { url: url, method: 'POST', args: args, - cacheAuthnState: true + saveAuthnState: true }; util.extend(postOptions, options); return httpRequest(sdk, postOptions); diff --git a/lib/tx.js b/lib/tx.js index be949e1b7..69c992ab7 100644 --- a/lib/tx.js +++ b/lib/tx.js @@ -79,7 +79,7 @@ function getPollFn(sdk, res, ref) { href += '?rememberDevice=true'; } return http.post(sdk, href, getStateToken(res), { - cacheAuthnState: false + saveAuthnState: false }); }