diff --git a/src/core/jwt/jwtTokenHandler.js b/src/core/jwt/jwtTokenHandler.js index 2d59d0b..68db4a6 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,12 +32,19 @@ 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 {Number} [options.accessTokenTTL] Set accessToken TTL in ms for token store + * @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, + accessTokenTTL, + useCredentials = false +} = {}) { const tokenStorage = jwtTokenStoreFactory({ - namespace: serviceName + namespace: serviceName, + accessTokenTTL }); /** @@ -51,29 +58,44 @@ 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'); - } else { - return fetch(refreshTokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ refreshToken }) - }) - .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)); + const unQueuedRefreshToken = () => { + let body; + let credentials; + let flow; + if (useCredentials) { + credentials = 'include'; + flow = Promise.resolve(); + } else { + flow = tokenStorage.getRefreshToken().then(refreshToken => { + if (!refreshToken) { + throw new Error('Refresh token is not available'); + } + body = JSON.stringify({ refreshToken }); + }); } - }); + + return flow + .then(() => + fetch(refreshTokenUrl, { + method: 'POST', + credentials, + headers: { + 'Content-Type': 'application/json' + }, + 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(({ accessToken }) => tokenStorage.setAccessToken(accessToken).then(() => accessToken)); + }; return { /** @@ -90,6 +112,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 +132,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)); }, @@ -132,6 +161,14 @@ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({serviceName = 't */ refreshToken() { return actionQueue.serie(() => unQueuedRefreshToken()); + }, + + /** + * Set accessToken TTL + * @param {Number} accessTokenTTL - accessToken TTL in ms + */ + setAccessTokenTTL(accessTokenTTL) { + tokenStorage.setAccessTokenTTL(accessTokenTTL); } }; }; 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; } }; }; 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