Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/tr 652 credential support for jwttokenhandler #89

Merged
merged 10 commits into from
Feb 24, 2021
87 changes: 62 additions & 25 deletions src/core/jwt/jwtTokenHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ;
*/

/**
Expand All @@ -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
});

/**
Expand All @@ -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<String>} 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 {
/**
Expand All @@ -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();
Expand All @@ -106,6 +132,9 @@ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({serviceName = 't
* @returns {Promise<Boolean>} Promise of token is stored
*/
storeRefreshToken(refreshToken) {
if (useCredentials) {
return Promise.resolve(false);
}
return actionQueue.serie(() => tokenStorage.setRefreshToken(refreshToken));
},

Expand All @@ -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);
}
};
};
Expand Down
24 changes: 22 additions & 2 deletions src/core/jwt/jwtTokenStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ;
*/

/**
Expand All @@ -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.
*/
Expand All @@ -48,6 +55,7 @@ const jwtTokenStoreFactory = function jwtTokenStoreFactory({namespace = 'global'
* @returns {Promise<Boolean>} token successfully set
*/
setAccessToken(token) {
accessTokenStoredAt = Date.now();
return getAccessTokenStore().then(storage => storage.setItem(accessTokenName, token));
},

Expand All @@ -56,6 +64,9 @@ const jwtTokenStoreFactory = function jwtTokenStoreFactory({namespace = 'global'
* @returns {Promise<string|null>} stored access token
*/
getAccessToken() {
if (accessTokenTTL && accessTokenStoredAt + accessTokenTTL < Date.now()) {
return Promise.resolve(null);
krampstudio marked this conversation as resolved.
Show resolved Hide resolved
}
return getAccessTokenStore().then(storage => storage.getItem(accessTokenName));
},

Expand Down Expand Up @@ -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;
}
};
};
Expand Down
17 changes: 13 additions & 4 deletions test/core/jwt/jwtTokenHandler/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@
<title>Test - JWT Token Handler</title>
<script type="text/javascript" src="/environment/require.js"></script>
<script type="text/javascript">
require(['/environment/config.js'], function() {
require(['qunitEnv'], function() {
require(['/environment/config.js'], function () {
require(['qunitEnv'], function () {
// mock jwt token store
define('core/jwt/jwtTokenStore', () => () => {
define('core/jwt/jwtTokenStore', () => ({ accessTokenTTL: accessTokenTTLParam }) => {
let accessToken = null;
let refreshToken = null;
let accessTokenTTL = accessTokenTTLParam;
let accessTokenStoredAt = 0;
return {
setAccessToken(newAccessToken) {
accessToken = newAccessToken;
accessTokenStoredAt = Date.now();
return Promise.resolve(true);
},
getAccessToken() {
if (accessTokenTTL && accessTokenStoredAt + accessTokenTTL < Date.now()) {
return Promise.resolve(null);
}
return Promise.resolve(accessToken);
},
setRefreshToken(newRefreshToken) {
Expand All @@ -29,10 +35,13 @@
clear() {
accessToken = refreshToken = null;
return Promise.resolve(true);
},
setAccessTokenTTL(newAccessTokenTTL) {
accessTokenTTL = newAccessTokenTTL;
}
};
});
require(['test/core/jwt/jwtTokenHandler/test'], function() {
require(['test/core/jwt/jwtTokenHandler/test'], function () {
QUnit.start();
});
});
Expand Down
Loading