diff --git a/package-lock.json b/package-lock.json index 852ad39..a8cf73b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-core-sdk", - "version": "1.18.2", + "version": "1.19.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6429fc6..d1322f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@oat-sa/tao-core-sdk", - "version": "1.18.2", + "version": "1.19.0", "displayName": "TAO Core SDK", "description": "Core libraries of TAO", "homepage": "https://github.com/oat-sa/tao-core-sdk-fe#readme", diff --git a/src/core/jwt/jwtTokenHandler.js b/src/core/jwt/jwtTokenHandler.js index 091bf09..91edec3 100644 --- a/src/core/jwt/jwtTokenHandler.js +++ b/src/core/jwt/jwtTokenHandler.js @@ -37,6 +37,7 @@ import TokenError from 'core/error/TokenError'; * @param {Boolean} [options.usePerTokenTTL] if true, accessToken TTL should be extractable from JWT payload, and accessTokenTTL will be used as fallback * @param {Boolean} [options.useCredentials] refreshToken stored in cookie instead of store * @param {Object} [options.refreshTokenParameters] Parameters that should be send in refreshToken call + * @param {Boolean} [options.oauth2RequestFormat] use oauth2 request format * @returns {Object} JWT Token handler instance */ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({ @@ -45,7 +46,8 @@ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({ accessTokenTTL, usePerTokenTTL = false, refreshTokenParameters, - useCredentials = false + useCredentials = false, + oauth2RequestFormat = false } = {}) { const tokenStorage = jwtTokenStoreFactory({ namespace: serviceName, @@ -65,12 +67,12 @@ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({ * @returns {Promise} Promise of new token */ const unQueuedRefreshToken = () => { - let body; + let parameters; let credentials; let flow; if (refreshTokenParameters) { - body = Object.assign({}, refreshTokenParameters); + parameters = Object.assign({}, refreshTokenParameters); } if (useCredentials) { @@ -81,21 +83,34 @@ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({ if (!refreshToken) { throw new Error('Refresh token is not available'); } - body = Object.assign({}, body, { refreshToken }); + if (oauth2RequestFormat) { + parameters = Object.assign({}, parameters, { refresh_token: refreshToken }); + } else { + parameters = Object.assign({}, parameters, { refreshToken }); + } }); } return flow .then(() => { - if (body) { - body = JSON.stringify(body); + const headers = {}; + let body; + if (oauth2RequestFormat) { + body = new FormData(); + Object.keys(parameters).forEach(key => { + body.append(key, parameters[key]); + }); + } else { + if (parameters) { + body = JSON.stringify(parameters); + } + + headers['Content-Type'] = 'application/json'; } return fetch(refreshTokenUrl, { method: 'POST', credentials, - headers: { - 'Content-Type': 'application/json' - }, + headers, body }); }) @@ -112,7 +127,28 @@ const jwtTokenHandlerFactory = function jwtTokenHandlerFactory({ error.response = response; return Promise.reject(error); }) - .then(({ accessToken }) => tokenStorage.setAccessToken(accessToken).then(() => accessToken)); + .then(response => { + let accessToken, refreshToken, expiresIn; + + if (oauth2RequestFormat) { + accessToken = response.access_token; + refreshToken = response.refresh_token; + expiresIn = response.expires_in; + } else { + accessToken = response.accessToken; + refreshToken = response.refreshToken; + } + + if (expiresIn) { + tokenStorage.setAccessTokenTTL(expiresIn * 1000); + } + + if (accessToken && refreshToken) { + return tokenStorage.setTokens(accessToken, refreshToken).then(() => accessToken); + } + + return tokenStorage.setAccessToken(accessToken).then(() => accessToken); + }); }; return { diff --git a/test/core/jwt/jwtTokenHandler/test.html b/test/core/jwt/jwtTokenHandler/test.html index 22ca8be..1a95cd4 100644 --- a/test/core/jwt/jwtTokenHandler/test.html +++ b/test/core/jwt/jwtTokenHandler/test.html @@ -38,6 +38,12 @@ }, setAccessTokenTTL(newAccessTokenTTL) { accessTokenTTL = newAccessTokenTTL; + }, + setTokens(newAccessToken, newRefreshToken) { + accessToken = newAccessToken; + refreshToken = newRefreshToken; + accessTokenStoredAt = Date.now(); + return Promise.resolve(true); } }; }); diff --git a/test/core/jwt/jwtTokenHandler/test.js b/test/core/jwt/jwtTokenHandler/test.js index ef032df..2af4ebc 100644 --- a/test/core/jwt/jwtTokenHandler/test.js +++ b/test/core/jwt/jwtTokenHandler/test.js @@ -532,4 +532,85 @@ define(['jquery', 'core/jwt/jwtTokenHandler', 'fetch-mock', 'core/error/TokenErr } ); }); + + QUnit.module('OAuth2', { + beforeEach: function () { + this.handler = jwtTokenHandlerFactory({ + refreshTokenUrl: '/refreshUrl', + oauth2RequestFormat: true, + refreshTokenParameters: { + grant_type: 'refresh_token', + client_id: 'client_1' + } + }); + }, + afterEach: function (assert) { + const done = assert.async(); + fetchMock.restore(); + this.handler.clearStore().then(done); + } + }); + + QUnit.test('refresh token', function (assert) { + assert.expect(10); + + const done = assert.async(); + + const accessToken = 'some access token'; + const refreshToken = 'some refresh token'; + const updatedRefreshToken = 'some updated refresh token'; + const updatedAccessToken = 'some updated access token'; + + const setupSecondRequest = () => { + fetchMock.restore(); + fetchMock.mock('/refreshUrl', function (url, opts) { + const data = {}; + for (let key of opts.body.keys()) { + data[key] = opts.body.get(key); + } + assert.equal(data.grant_type, 'refresh_token', 'grant type is sent to the api'); + assert.equal(data.client_id, 'client_1', 'client id is sent to the api'); + assert.equal(data.refresh_token, updatedRefreshToken, 'new refresh token is sent to the api'); + return JSON.stringify({ + access_token: updatedAccessToken, + refresh_token: updatedRefreshToken, + expires_in: 1 + }); + }); + }; + + fetchMock.mock('/refreshUrl', function (url, opts) { + const data = {}; + for (let key of opts.body.keys()) { + data[key] = opts.body.get(key); + } + assert.equal(data.grant_type, 'refresh_token', 'grant type is sent to the api'); + assert.equal(data.client_id, 'client_1', 'client id is sent to the api'); + assert.equal(data.refresh_token, refreshToken, 'refresh token is sent to the api'); + setupSecondRequest(); + return JSON.stringify({ + access_token: accessToken, + refresh_token: updatedRefreshToken, + expires_in: 1 + }); + }); + + this.handler.storeRefreshToken(refreshToken).then(setTokenResult => { + assert.equal(setTokenResult, true, 'refresh token is set'); + 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'); + }).then(() => new Promise(resolve => { + setTimeout(resolve, 1000); + })).then(() => { + this.handler.getToken().then(storedAccessToken => { + assert.equal(storedAccessToken, updatedAccessToken, 'get access token again, because it was expired'); + done(); + }); + }); + }); + }); + }); });