From 1d3ebbee0fa1476ddd3cf67ed67f53b9264adf0d Mon Sep 17 00:00:00 2001 From: Tamas Besenyei Date: Fri, 19 Feb 2021 13:41:29 +0100 Subject: [PATCH 01/10] feat: implement useCredentials --- src/core/jwt/jwtTokenHandler.js | 64 ++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/src/core/jwt/jwtTokenHandler.js b/src/core/jwt/jwtTokenHandler.js index 2d59d0b..b7c0841 100644 --- a/src/core/jwt/jwtTokenHandler.js +++ b/src/core/jwt/jwtTokenHandler.js @@ -13,7 +13,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2019 (original work) Open Assessment Technologies SA ; + * Copyright (c) 2019-2021 (original work) Open Assessment Technologies SA ; */ /** @@ -32,10 +32,14 @@ import promiseQueue from 'core/promiseQueue'; * @param {Object} options Options of JWT token handler * @param {String} options.serviceName Name of the service what JWT token belongs to * @param {String} options.refreshTokenUrl Url where handler could refresh JWT token + * @param {Boolean} [options.useCredentials] refreshToken stored in cookie instead of store * @returns {Object} JWT Token handler instance */ -const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({serviceName = 'tao', refreshTokenUrl} = {}) { - +const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({ + serviceName = 'tao', + refreshTokenUrl, + useCredentials = false +} = {}) { const tokenStorage = jwtTokenStoreFactory({ namespace: serviceName }); @@ -51,29 +55,42 @@ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({serviceName = 't * It will refresh the token from provided API and saves it for later use * @returns {Promise} Promise of new token */ - const unQueuedRefreshToken = () => tokenStorage.getRefreshToken().then(refreshToken => { - if (!refreshToken) { - throw new Error('Refresh token is not available'); + const unQueuedRefreshToken = () => { + let body; + let credentials; + let flow; + + if (useCredentials) { + credentials = 'include'; + flow = Promise.resolve(); } else { - return fetch(refreshTokenUrl, { + flow = tokenStorage.getRefreshToken().then(refreshToken => { + if (!refreshToken) { + throw new Error('Refresh token is not available'); + } else { + body = JSON.stringify({ refreshToken }); + } + }); + } + + return flow.then(() => fetch(refreshTokenUrl, { method: 'POST', + credentials, headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refreshToken }) + body + })) + .then(response => { + if (response.status === 200) { + return response.json(); + } + const error = new Error('Unsuccessful token refresh'); + error.response = response; + return Promise.reject(error); }) - .then(response => { - if (response.status === 200) { - return response.json(); - } - const error = new Error('Unsuccessful token refresh'); - error.response = response; - return Promise.reject(error); - }) - .then(({ accessToken }) => tokenStorage.setAccessToken(accessToken).then(() => accessToken)); - - } - }); + .then(({ accessToken }) => tokenStorage.setAccessToken(accessToken).then(() => accessToken)); + }; return { /** @@ -90,6 +107,10 @@ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({serviceName = 't if (accessToken) { return accessToken; } + + if (useCredentials) { + return unQueuedRefreshToken(); + } return tokenStorage.getRefreshToken().then(refreshToken => { if (refreshToken) { return unQueuedRefreshToken(); @@ -106,6 +127,9 @@ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({serviceName = 't * @returns {Promise} Promise of token is stored */ storeRefreshToken(refreshToken) { + if (useCredentials) { + return Promise.resolve(false); + } return actionQueue.serie(() => tokenStorage.setRefreshToken(refreshToken)); }, From b653d8574eb074c3b3e6d13bbe67051ea234e14a Mon Sep 17 00:00:00 2001 From: Tamas Besenyei Date: Fri, 19 Feb 2021 14:17:31 +0100 Subject: [PATCH 02/10] test: useCredential case --- test/core/jwt/jwtTokenHandler/test.js | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/core/jwt/jwtTokenHandler/test.js b/test/core/jwt/jwtTokenHandler/test.js index 5e3f3cf..5e800c6 100644 --- a/test/core/jwt/jwtTokenHandler/test.js +++ b/test/core/jwt/jwtTokenHandler/test.js @@ -304,4 +304,49 @@ define(['jquery', 'core/jwt/jwtTokenHandler', 'fetch-mock'], ($, jwtTokenHandler Promise.all([refreshTokenPromise1, refreshTokenPromise2]).then(done); }); }); + + QUnit.module('useCredential', { + beforeEach: function() { + this.handler = jwtTokenHandlerFactory({ refreshTokenUrl: '/refreshUrl', useCredentials: true }); + }, + afterEach: function(assert) { + const done = assert.async(); + fetchMock.restore(); + this.handler.clearStore().then(done); + } + }); + + QUnit.test('refresh token', function(assert) { + assert.expect(4); + + const done = assert.async(); + + const accessToken = 'some access token'; + + fetchMock.mock('/refreshUrl', function(url, opts) { + assert.equal(opts.credentials, 'include', 'credentials are sent to the api'); + assert.equal(typeof opts.body, 'undefined', 'body is undefined'); + return JSON.stringify({ accessToken }); + }); + + this.handler.getToken().then(refreshedAccessToken => { + assert.equal(refreshedAccessToken, accessToken, 'get refreshed access token'); + + this.handler.getToken().then(storedAccessToken => { + assert.equal(storedAccessToken, accessToken, 'get access token from store without refresh'); + done(); + }); + }); + }); + + QUnit.test('cannot set refresh token', function(assert) { + assert.expect(1); + + const done = assert.async(); + + this.handler.storeRefreshToken('refreshToken').then(setTokenResult => { + assert.equal(setTokenResult, false, 'refresh token is not set'); + done(); + }); + }); }); From 1a97b29696d1de37838a20879449edf0220dee32 Mon Sep 17 00:00:00 2001 From: Tamas Besenyei Date: Fri, 19 Feb 2021 14:18:37 +0100 Subject: [PATCH 03/10] chore: update license header --- test/core/jwt/jwtTokenHandler/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core/jwt/jwtTokenHandler/test.js b/test/core/jwt/jwtTokenHandler/test.js index 5e800c6..1a02a53 100644 --- a/test/core/jwt/jwtTokenHandler/test.js +++ b/test/core/jwt/jwtTokenHandler/test.js @@ -13,7 +13,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2019 (original work) Open Assessment Technologies SA ; + * Copyright (c) 2019-2021 (original work) Open Assessment Technologies SA ; */ /** From 16948bcf634a4fc0d849b3b04e0d55702677371e Mon Sep 17 00:00:00 2001 From: Tamas Besenyei Date: Mon, 22 Feb 2021 09:52:25 +0100 Subject: [PATCH 04/10] feat: support acessTokenTTL in jwtTokenStore --- src/core/jwt/jwtTokenStore.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/core/jwt/jwtTokenStore.js b/src/core/jwt/jwtTokenStore.js index 1191103..5088b7b 100644 --- a/src/core/jwt/jwtTokenStore.js +++ b/src/core/jwt/jwtTokenStore.js @@ -13,7 +13,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2019 (original work) Open Assessment Technologies SA ; + * Copyright (c) 2019-2021 (original work) Open Assessment Technologies SA ; */ /** @@ -27,13 +27,20 @@ import store from 'core/store'; /** * @param {Object} options - Factory options * @param {string} options.namespace - Namespace of the store + * @param {Number} options.accessTokenTTL - TTL of accessToken in ms * @returns {Object} Store API */ -const jwtTokenStoreFactory = function jwtTokenStoreFactory({namespace = 'global'} = {}) { +const jwtTokenStoreFactory = function jwtTokenStoreFactory({ + namespace = 'global', + accessTokenTTL: accessTokenTTLParam +} = {}) { const storeName = `jwt.${namespace}`; const accessTokenName = 'accessToken'; const refreshTokenName = 'refreshToken'; + let accessTokenTTL = accessTokenTTLParam; + let accessTokenStoredAt = 0; + /** * Do not change token stores, because of security reason. */ @@ -48,6 +55,7 @@ const jwtTokenStoreFactory = function jwtTokenStoreFactory({namespace = 'global' * @returns {Promise} token successfully set */ setAccessToken(token) { + accessTokenStoredAt = Date.now(); return getAccessTokenStore().then(storage => storage.setItem(accessTokenName, token)); }, @@ -56,6 +64,9 @@ const jwtTokenStoreFactory = function jwtTokenStoreFactory({namespace = 'global' * @returns {Promise} stored access token */ getAccessToken() { + if (accessTokenTTL && accessTokenStoredAt + accessTokenTTL < Date.now()) { + return Promise.resolve(null); + } return getAccessTokenStore().then(storage => storage.getItem(accessTokenName)); }, @@ -108,6 +119,15 @@ const jwtTokenStoreFactory = function jwtTokenStoreFactory({namespace = 'global' */ clear() { return Promise.all([this.clearAccessToken(), this.clearRefreshToken()]).then(() => true); + }, + + /** + * Set a new TTL value for accessToken + * @param {Number} newAccessTokenTTL - accessToken TTL in ms + * @returns {void} + */ + setAccessTokenTTL(newAccessTokenTTL) { + accessTokenTTL = newAccessTokenTTL; } }; }; From c289631f21edebdfb0de4acad34e3e60773fb902 Mon Sep 17 00:00:00 2001 From: Tamas Besenyei Date: Mon, 22 Feb 2021 10:05:42 +0100 Subject: [PATCH 05/10] test: setAccessTokenTTL in store --- test/core/jwt/jwtTokenStore/test.js | 87 +++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/test/core/jwt/jwtTokenStore/test.js b/test/core/jwt/jwtTokenStore/test.js index 9896211..a4f24f4 100644 --- a/test/core/jwt/jwtTokenStore/test.js +++ b/test/core/jwt/jwtTokenStore/test.js @@ -41,10 +41,10 @@ define(['core/jwt/jwtTokenStore'], jwtTokenStoreFactory => { }); QUnit.module('API', { - beforeEach: function() { + beforeEach: function () { this.storage = jwtTokenStoreFactory(); }, - afterEach: function(assert) { + afterEach: function (assert) { const done = assert.async(); this.storage.clear().then(done); } @@ -52,7 +52,7 @@ define(['core/jwt/jwtTokenStore'], jwtTokenStoreFactory => { const exampleTokens = ['foo', 'bar', 'baz']; - QUnit.cases.init(exampleTokens).test('set access token', function(token, assert) { + QUnit.cases.init(exampleTokens).test('set access token', function (token, assert) { assert.expect(2); const done = assert.async(); @@ -65,7 +65,7 @@ define(['core/jwt/jwtTokenStore'], jwtTokenStoreFactory => { }); }); - QUnit.cases.init(exampleTokens).test('set refresh token', function(token, assert) { + QUnit.cases.init(exampleTokens).test('set refresh token', function (token, assert) { assert.expect(2); const done = assert.async(); @@ -78,7 +78,7 @@ define(['core/jwt/jwtTokenStore'], jwtTokenStoreFactory => { }); }); - QUnit.test('set tokens', function(assert) { + QUnit.test('set tokens', function (assert) { assert.expect(3); const done = assert.async(); @@ -97,7 +97,7 @@ define(['core/jwt/jwtTokenStore'], jwtTokenStoreFactory => { }); }); - QUnit.test('clear access token', function(assert) { + QUnit.test('clear access token', function (assert) { assert.expect(3); const done = assert.async(); @@ -113,7 +113,7 @@ define(['core/jwt/jwtTokenStore'], jwtTokenStoreFactory => { }); }); - QUnit.test('clear refresh token', function(assert) { + QUnit.test('clear refresh token', function (assert) { assert.expect(3); const done = assert.async(); @@ -129,7 +129,7 @@ define(['core/jwt/jwtTokenStore'], jwtTokenStoreFactory => { }); }); - QUnit.test('clear tokens', function(assert) { + QUnit.test('clear tokens', function (assert) { assert.expect(4); const done = assert.async(); @@ -148,18 +148,73 @@ define(['core/jwt/jwtTokenStore'], jwtTokenStoreFactory => { }); }); + QUnit.test('accessTokenTTL in constructor', function (assert) { + const done = assert.async(); + assert.expect(3); + + const accessToken = 'foo'; + + const storage = jwtTokenStoreFactory({ + accessTokenTTL: 500 + }); + + storage + .setAccessToken(accessToken) + .then(storeResult => { + assert.ok(storeResult, 'accessToken is stored'); + return storage.getAccessToken(); + }) + .then(storedAccessToken => { + assert.equal(storedAccessToken, accessToken, 'accessToken can be received before ttl'); + return new Promise(resolve => setTimeout(resolve, 500)); + }) + .then(storage.getAccessToken) + .then(storedAccessToken => { + assert.equal(storedAccessToken, null, 'accessToken cannot be received after ttl'); + done(); + }); + }); + + QUnit.test('setAccessTokenTTL', function (assert) { + const done = assert.async(); + assert.expect(3); + + const accessToken = 'foo'; + + const storage = jwtTokenStoreFactory({ + accessTokenTTL: 1000 + }); + + storage + .setAccessToken(accessToken) + .then(storeResult => { + assert.ok(storeResult, 'accessToken is stored'); + return storage.getAccessToken(); + }) + .then(storedAccessToken => { + assert.equal(storedAccessToken, accessToken, 'accessToken can be received before ttl'); + storage.setAccessTokenTTL(100); + return new Promise(resolve => setTimeout(resolve, 100)); + }) + .then(storage.getAccessToken) + .then(storedAccessToken => { + assert.equal(storedAccessToken, null, 'accessToken cannot be received after ttl'); + done(); + }); + }); + QUnit.module('Same namespace', { - beforeEach: function() { + beforeEach: function () { this.storage1 = jwtTokenStoreFactory({ namespace: 'namespace' }); this.storage2 = jwtTokenStoreFactory({ namespace: 'namespace' }); }, - afterEach: function(assert) { + afterEach: function (assert) { const done = assert.async(); Promise.all([this.storage1.clear(), this.storage2.clear()]).then(done); } }); - QUnit.test('stores could access to same tokens', function(assert) { + QUnit.test('stores could access to same tokens', function (assert) { assert.expect(3); const done = assert.async(); @@ -178,7 +233,7 @@ define(['core/jwt/jwtTokenStore'], jwtTokenStoreFactory => { }); }); - QUnit.test('store will be empty if other store will be cleared', function(assert) { + QUnit.test('store will be empty if other store will be cleared', function (assert) { assert.expect(4); const done = assert.async(); @@ -201,17 +256,17 @@ define(['core/jwt/jwtTokenStore'], jwtTokenStoreFactory => { }); QUnit.module('Different namespace', { - beforeEach: function() { + beforeEach: function () { this.storage1 = jwtTokenStoreFactory({ namespace: 'namespace1' }); this.storage2 = jwtTokenStoreFactory({ namespace: 'namespace2' }); }, - afterEach: function(assert) { + afterEach: function (assert) { const done = assert.async(); Promise.all([this.storage1.clear(), this.storage2.clear()]).then(done); } }); - QUnit.test('stores could not access to same tokens', function(assert) { + QUnit.test('stores could not access to same tokens', function (assert) { assert.expect(6); const done = assert.async(); @@ -242,7 +297,7 @@ define(['core/jwt/jwtTokenStore'], jwtTokenStoreFactory => { }); }); - QUnit.test('store content will be kept if other store will be cleared', function(assert) { + QUnit.test('store content will be kept if other store will be cleared', function (assert) { assert.expect(4); const done = assert.async(); From d537b73867b560596c97c0ff1bda7b1c66065170 Mon Sep 17 00:00:00 2001 From: Tamas Besenyei Date: Mon, 22 Feb 2021 10:06:16 +0100 Subject: [PATCH 06/10] feat: accessTokenTTL support for jwtTokenHandler --- src/core/jwt/jwtTokenHandler.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/core/jwt/jwtTokenHandler.js b/src/core/jwt/jwtTokenHandler.js index b7c0841..43a4398 100644 --- a/src/core/jwt/jwtTokenHandler.js +++ b/src/core/jwt/jwtTokenHandler.js @@ -38,10 +38,12 @@ import promiseQueue from 'core/promiseQueue'; const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({ serviceName = 'tao', refreshTokenUrl, + accessTokenTTL, useCredentials = false } = {}) { const tokenStorage = jwtTokenStoreFactory({ - namespace: serviceName + namespace: serviceName, + accessTokenTTL }); /** @@ -156,6 +158,14 @@ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({ */ refreshToken() { return actionQueue.serie(() => unQueuedRefreshToken()); + }, + + /** + * Set accessToken TTL + * @param {Number} accessTokenTTL - accessToken TTL in ms + */ + setAccessTokenTTL(accessTokenTTL) { + tokenStorage.setAccessTokenTTL(accessTokenTTL); } }; }; From 038f77137776cfc1a2672057b9bb5ac8533de337 Mon Sep 17 00:00:00 2001 From: Tamas Besenyei Date: Mon, 22 Feb 2021 10:28:35 +0100 Subject: [PATCH 07/10] test: jwtToken ttl support --- test/core/jwt/jwtTokenHandler/test.html | 17 ++- test/core/jwt/jwtTokenHandler/test.js | 186 +++++++++++++++++------- test/core/jwt/jwtTokenStore/test.js | 2 +- 3 files changed, 147 insertions(+), 58 deletions(-) diff --git a/test/core/jwt/jwtTokenHandler/test.html b/test/core/jwt/jwtTokenHandler/test.html index 93ac5e3..22ca8be 100644 --- a/test/core/jwt/jwtTokenHandler/test.html +++ b/test/core/jwt/jwtTokenHandler/test.html @@ -5,18 +5,24 @@ Test - JWT Token Handler