From 011309a1033c9a3346bb5e169e972965ddf153fc Mon Sep 17 00:00:00 2001 From: Aschen Date: Fri, 26 Apr 2019 16:56:16 +0200 Subject: [PATCH 01/12] Add isLoggued --- src/Kuzzle.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Kuzzle.js b/src/Kuzzle.js index 9382ddb96..19051417a 100644 --- a/src/Kuzzle.js +++ b/src/Kuzzle.js @@ -203,6 +203,15 @@ class Kuzzle extends KuzzleEventEmitter { return this.protocol.sslConnection; } + isLoggued () { + if (!this.jwt) { + return false; + } + + return this.auth.checkToken(this.jwt) + .then(({ valid }) => valid); + } + /** * Emit an event to all registered listeners * An event cannot be emitted multiple times before a timeout has been reached. From ea134e162ef2ca5c9e17b1fb0a2509551342ab2b Mon Sep 17 00:00:00 2001 From: Aschen Date: Sun, 28 Apr 2019 13:00:43 +0200 Subject: [PATCH 02/12] Offline isLoggued --- src/Kuzzle.js | 3 +-- src/controllers/auth.js | 17 ++++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Kuzzle.js b/src/Kuzzle.js index 19051417a..a687823af 100644 --- a/src/Kuzzle.js +++ b/src/Kuzzle.js @@ -208,8 +208,7 @@ class Kuzzle extends KuzzleEventEmitter { return false; } - return this.auth.checkToken(this.jwt) - .then(({ valid }) => valid); + return this.jwtExpiresAt > Date.now(); } /** diff --git a/src/controllers/auth.js b/src/controllers/auth.js index 342a150db..c8350b323 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -144,18 +144,19 @@ class AuthController extends BaseController { throw new Error('Kuzzle.auth.login: strategy is required'); } - const - request = { - strategy, - expiresIn, - body: credentials, - action: 'login' - }; + const request = { + strategy, + expiresIn, + body: credentials, + action: 'login' + }; return this.query(request, {queuable: false}) .then(response => { try { this.kuzzle.jwt = response.result.jwt; + this.kuzzle.jwtExpiresAt = response.result.expiresAt; + this.kuzzle.emit('loginAttempt', {success: true}); } catch (err) { @@ -180,6 +181,7 @@ class AuthController extends BaseController { }, {queuable: false}) .then(() => { this.kuzzle.jwt = undefined; + this.kuzzle.jwtExpiresAt = undefined; }); } @@ -247,6 +249,7 @@ class AuthController extends BaseController { return this.query(query, options) .then(response => { this.kuzzle.jwt = response.result.jwt; + this.kuzzle.jwtExpiresAt = response.result.expiresAt; return response.result; }); From 799a2f2d51038ef32ba5c1b9623661c2568e39d3 Mon Sep 17 00:00:00 2001 From: Aschen Date: Sun, 28 Apr 2019 13:02:52 +0200 Subject: [PATCH 03/12] unset jwtExpiresAt --- src/Kuzzle.js | 3 +++ src/controllers/realtime/index.js | 1 + 2 files changed, 4 insertions(+) diff --git a/src/Kuzzle.js b/src/Kuzzle.js index a687823af..5d16cee69 100644 --- a/src/Kuzzle.js +++ b/src/Kuzzle.js @@ -247,6 +247,7 @@ class Kuzzle extends KuzzleEventEmitter { this.protocol.addListener('tokenExpired', () => { this.jwt = undefined; + this.jwtExpiresAt = undefined; this.emit('tokenExpired'); }); @@ -288,10 +289,12 @@ class Kuzzle extends KuzzleEventEmitter { // shouldn't obtain an error but let's invalidate the token anyway if (!res.valid) { this.jwt = undefined; + this.jwtExpiresAt = undefined; } }) .catch(() => { this.jwt = undefined; + this.jwtExpiresAt = undefined; }) .then(() => this.emit('reconnected')); } diff --git a/src/controllers/realtime/index.js b/src/controllers/realtime/index.js index 9913fc583..cd3145a22 100644 --- a/src/controllers/realtime/index.js +++ b/src/controllers/realtime/index.js @@ -112,6 +112,7 @@ class RealTimeController extends BaseController { this.subscriptions = {}; this.kuzzle.jwt = undefined; + this.kuzzle.jwtExpiresAt = undefined; const now = Date.now(); if ((now - this.lastExpirationTimestamp) > expirationThrottleDelay) { From 3a6550195e9cdd0a402681d07fc2641f68ccd334 Mon Sep 17 00:00:00 2001 From: Aschen Date: Fri, 3 May 2019 16:25:11 +0200 Subject: [PATCH 04/12] fix tests --- features/steps/collection.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/features/steps/collection.js b/features/steps/collection.js index 440b8412f..677b3ed9d 100644 --- a/features/steps/collection.js +++ b/features/steps/collection.js @@ -108,10 +108,8 @@ Then(/^the collection(?: '(.*?)')? should exist$/, async function (collection) { Then('the mapping of {string} should be updated', async function (collection) { const mapping = await this.kuzzle.collection.getMapping(this.index, collection); - should(mapping[this.index].mappings[collection]).eql({ - properties: { - gordon: {type: 'keyword'} - } + should(mapping[this.index].mappings[collection].properties).eql({ + gordon: { type: 'keyword' } }); }); @@ -129,7 +127,6 @@ Then('the specifications of {string} must not exist', async function (collection catch (error) { should(error.status).eql(404); } - }); Then('they should be validated', function () { From dad0cb97413afad8b6ef79c29dc7d8b308c82551 Mon Sep 17 00:00:00 2001 From: Aschen Date: Mon, 6 May 2019 17:37:14 +0200 Subject: [PATCH 05/12] rename to authenticated --- src/Kuzzle.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Kuzzle.js b/src/Kuzzle.js index 5d16cee69..5cfc84b86 100644 --- a/src/Kuzzle.js +++ b/src/Kuzzle.js @@ -203,12 +203,8 @@ class Kuzzle extends KuzzleEventEmitter { return this.protocol.sslConnection; } - isLoggued () { - if (!this.jwt) { - return false; - } - - return this.jwtExpiresAt > Date.now(); + get authenticated () { + return this.jwt !== null && this.jwt !== undefined; } /** @@ -247,7 +243,6 @@ class Kuzzle extends KuzzleEventEmitter { this.protocol.addListener('tokenExpired', () => { this.jwt = undefined; - this.jwtExpiresAt = undefined; this.emit('tokenExpired'); }); @@ -289,13 +284,11 @@ class Kuzzle extends KuzzleEventEmitter { // shouldn't obtain an error but let's invalidate the token anyway if (!res.valid) { this.jwt = undefined; - this.jwtExpiresAt = undefined; - } + } }) .catch(() => { this.jwt = undefined; - this.jwtExpiresAt = undefined; - }) + }) .then(() => this.emit('reconnected')); } From c7492744a487b9230baf9b5690f102f7a486cd86 Mon Sep 17 00:00:00 2001 From: Aschen Date: Mon, 6 May 2019 17:44:43 +0200 Subject: [PATCH 06/12] lint --- src/Kuzzle.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Kuzzle.js b/src/Kuzzle.js index 5cfc84b86..955aaa3e1 100644 --- a/src/Kuzzle.js +++ b/src/Kuzzle.js @@ -284,11 +284,11 @@ class Kuzzle extends KuzzleEventEmitter { // shouldn't obtain an error but let's invalidate the token anyway if (!res.valid) { this.jwt = undefined; - } + } }) .catch(() => { this.jwt = undefined; - }) + }) .then(() => this.emit('reconnected')); } From 0f2e49ee688f76e9018006b64493e1e8d206a3cc Mon Sep 17 00:00:00 2001 From: Aschen Date: Tue, 7 May 2019 11:28:18 +0200 Subject: [PATCH 07/12] Extract jwt from kuzzle object --- src/Kuzzle.js | 58 +++++++------------- src/controllers/auth.js | 54 +++++++++++++++---- test/controllers/auth.test.js | 95 +++++++++++++++++++++++---------- test/core/Jwt.test.js | 34 ++++++++++++ test/kuzzle/connect.test.js | 19 ++++--- test/kuzzle/constructor.test.js | 2 +- test/kuzzle/getters.test.js | 18 +++++-- test/kuzzle/protocol.test.js | 5 +- test/kuzzle/query.test.js | 6 ++- test/kuzzle/setters.test.js | 37 ++----------- test/mocks/generateJwt.mock.js | 14 +++++ 11 files changed, 219 insertions(+), 123 deletions(-) create mode 100644 test/core/Jwt.test.js create mode 100644 test/mocks/generateJwt.mock.js diff --git a/src/Kuzzle.js b/src/Kuzzle.js index 955aaa3e1..1434f7b92 100644 --- a/src/Kuzzle.js +++ b/src/Kuzzle.js @@ -116,28 +116,6 @@ class Kuzzle extends KuzzleEventEmitter { this._autoReplay = value; } - get jwt () { - return this._jwt; - } - - set jwt (token) { - if (token === undefined || token === null) { - this._jwt = undefined; - } - else if (typeof token === 'string') { - this._jwt = token; - } - else if (typeof token === 'object' - && token.result - && token.result.jwt - && typeof token.result.jwt === 'string' - ) { - this._jwt = token.result.jwt; - } else { - throw new Error(`Invalid token argument: ${token}`); - } - } - get host () { return this.protocol.host; } @@ -204,7 +182,19 @@ class Kuzzle extends KuzzleEventEmitter { } get authenticated () { - return this.jwt !== null && this.jwt !== undefined; + return this.auth.authenticationToken && !this.auth.authenticationToken.expired; + } + + get jwt () { + if (!this.auth.authenticationToken) { + return null; + } + + return this.auth.authenticationToken.encodedJwt; + } + + set jwt (encodedJwt) { + this.auth.authenticationToken = encodedJwt; } /** @@ -242,7 +232,7 @@ class Kuzzle extends KuzzleEventEmitter { this.protocol.addListener('queryError', (err, query) => this.emit('queryError', err, query)); this.protocol.addListener('tokenExpired', () => { - this.jwt = undefined; + this.auth.authenticationToken = null; this.emit('tokenExpired'); }); @@ -278,16 +268,17 @@ class Kuzzle extends KuzzleEventEmitter { this.playQueue(); } - if (this.jwt) { - return this.auth.checkToken(this.jwt) + if (this.auth.authenticationToken) { + return this.auth.checkToken() .then(res => { + // shouldn't obtain an error but let's invalidate the token anyway if (!res.valid) { - this.jwt = undefined; + this.auth.authenticationToken = null; } }) .catch(() => { - this.jwt = undefined; + this.auth.authenticationToken = null; }) .then(() => this.emit('reconnected')); } @@ -375,16 +366,7 @@ class Kuzzle extends KuzzleEventEmitter { request.volatile.sdkInstanceId = this.protocol.id; request.volatile.sdkVersion = this.sdkVersion; - /* - * Do not add the token for the checkToken route, to avoid getting a token error when - * a developer simply wish to verify his token - */ - if (this.jwt !== undefined - && !(request.controller === 'auth' - && (request.action === 'checkToken' || request.action === 'login')) - ) { - request.jwt = this.jwt; - } + this.auth.authenticateRequest(request); let queuable = true; if (options && options.queuable === false) { diff --git a/src/controllers/auth.js b/src/controllers/auth.js index c8350b323..47e954121 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -1,4 +1,5 @@ const + Jwt = require('../core/Jwt'), BaseController = require('./base'), User = require('./security/user'); @@ -16,6 +17,39 @@ class AuthController extends BaseController { */ constructor (kuzzle) { super(kuzzle, 'auth'); + + this._authenticationToken = null; + } + + get authenticationToken () { + return this._authenticationToken; + } + + set authenticationToken (encodedJwt) { + if (encodedJwt === undefined || encodedJwt === null) { + this._authenticationToken = null; + } else if (typeof encodedJwt === 'string') { + this._authenticationToken = new Jwt(encodedJwt); + } else { + throw new Error(`Invalid token argument: ${encodedJwt}`); + } + } + + /** + * Do not add the token for the checkToken route, to avoid getting a token error when + * a developer simply wish to verify his token + * + * @param {object} request + */ + authenticateRequest (request) { + if (!this.authenticationToken + || (request.controller === 'auth' + && (request.action === 'checkToken' || request.action === 'login')) + ) { + return; + } + + request.jwt = this.authenticationToken.encodedJwt; } /** @@ -24,11 +58,15 @@ class AuthController extends BaseController { * @param {string} token The jwt token to check * @return {Promise|*|PromiseLike|Promise} */ - checkToken (token) { + checkToken (token = undefined) { + if (token === undefined && this.authenticationToken) { + token = this.authenticationToken.encodedJwt; + } + return this.query({ action: 'checkToken', - body: {token} - }, {queuable: false}) + body: { token } + }, { queuable: false }) .then(response => response.result); } @@ -154,14 +192,14 @@ class AuthController extends BaseController { return this.query(request, {queuable: false}) .then(response => { try { - this.kuzzle.jwt = response.result.jwt; - this.kuzzle.jwtExpiresAt = response.result.expiresAt; + this._authenticationToken = new Jwt(response.result.jwt); this.kuzzle.emit('loginAttempt', {success: true}); } catch (err) { return Promise.reject(err); } + return response.result.jwt; }) .catch(err => { @@ -180,8 +218,7 @@ class AuthController extends BaseController { action: 'logout' }, {queuable: false}) .then(() => { - this.kuzzle.jwt = undefined; - this.kuzzle.jwtExpiresAt = undefined; + this._authenticationToken = null; }); } @@ -248,8 +285,7 @@ class AuthController extends BaseController { return this.query(query, options) .then(response => { - this.kuzzle.jwt = response.result.jwt; - this.kuzzle.jwtExpiresAt = response.result.expiresAt; + this._authenticationToken = new Jwt(response.result.jwt); return response.result; }); diff --git a/test/controllers/auth.test.js b/test/controllers/auth.test.js index 707036ff4..20b7918cc 100644 --- a/test/controllers/auth.test.js +++ b/test/controllers/auth.test.js @@ -1,12 +1,16 @@ const + Jwt = require('../../src/core/Jwt'), AuthController = require('../../src/controllers/auth'), User = require('../../src/controllers/security/user'), + generateJwt = require('../mocks/generateJwt.mock'), sinon = require('sinon'), should = require('should'); describe('Auth Controller', () => { const options = {opt: 'in'}; - let kuzzle; + let + jwt, + kuzzle; beforeEach(() => { kuzzle = { @@ -16,7 +20,7 @@ describe('Auth Controller', () => { kuzzle.auth = new AuthController(kuzzle); }); - describe('checkToken', () => { + describe('#checkToken', () => { it('should call auth/checkToken query with the token and return a Promise which resolves the token validity', () => { kuzzle.query.resolves({ result: { @@ -46,7 +50,7 @@ describe('Auth Controller', () => { }); }); - describe('createMyCredentials', () => { + describe('#createMyCredentials', () => { it('should call auth/createMyCredentials query with the user credentials and return a Promise which resolves a json object', () => { const credentials = {foo: 'bar'}; @@ -73,7 +77,7 @@ describe('Auth Controller', () => { }); }); - describe('credentialsExist', () => { + describe('#credentialsExist', () => { it('should call auth/credentialExists query with the strategy name and return a Promise which resolves a boolean', () => { kuzzle.query.resolves({result: true}); @@ -92,7 +96,7 @@ describe('Auth Controller', () => { }); }); - describe('deleteMyCredentials', () => { + describe('#deleteMyCredentials', () => { it('should call auth/deleteMyCredentials query with the strategy name and return a Promise which resolves an acknowledgement', () => { kuzzle.query.resolves({result: {acknowledged: true}}); @@ -111,7 +115,7 @@ describe('Auth Controller', () => { }); }); - describe('getCurrentUser', () => { + describe('#getCurrentUser', () => { it('should call auth/getCurrentUser query and return a Promise which resolves a User object', () => { kuzzle.query.resolves({ result: { @@ -136,7 +140,7 @@ describe('Auth Controller', () => { }); }); - describe('getMyCredentials', () => { + describe('#getMyCredentials', () => { it('should call auth/getMyCredentials query with the strategy name and return a Promise which resolves the user credentials', () => { kuzzle.query.resolves({ result: { @@ -160,7 +164,7 @@ describe('Auth Controller', () => { }); }); - describe('getMyRights', () => { + describe('#getMyRights', () => { it('should call auth/getMyCredentials query with the strategy name and return a Promise which resolves the user permissions as an array', () => { kuzzle.query.resolves({result: {hits: [ {controller: 'foo', action: 'bar', index: 'foobar', collection: '*', value: 'allowed'} @@ -181,7 +185,7 @@ describe('Auth Controller', () => { }); }); - describe('getStrategies', () => { + describe('#getStrategies', () => { it('should call auth/getStrategies query and return a Promise which resolves the list of strategies as an array', () => { kuzzle.query.resolves({result: ['local', 'github', 'foo', 'bar']}); @@ -203,14 +207,16 @@ describe('Auth Controller', () => { }); }); - describe('login', () => { + describe('#login', () => { const credentials = {foo: 'bar'}; beforeEach(() => { + jwt = generateJwt(); + kuzzle.query.resolves({ result: { - _id: 'kuid', - jwt: 'jwt' + jwt, + _id: 'kuid' } }); }); @@ -240,7 +246,7 @@ describe('Auth Controller', () => { body: credentials }, {queuable: false}); - should(res).be.equal('jwt'); + should(res).be.equal(jwt); }); }); @@ -253,17 +259,18 @@ describe('Auth Controller', () => { }); }); - it('should set the received JWT as kuzzle property', () => { + it('should construct a new Jwt', () => { return kuzzle.auth.login('strategy', credentials, 'expiresIn') .then(() => { - should(kuzzle.jwt).be.equal('jwt'); + should(kuzzle.auth.authenticationToken).not.be.null(); + should(kuzzle.auth.authenticationToken).be.instanceOf(Jwt); }); }); }); - describe('logout', () => { + describe('#logout', () => { beforeEach(() => { - kuzzle.jwt = 'jwt'; + kuzzle.auth.authenticationToken = generateJwt(); kuzzle.query.resolves({result: {aknowledged: true}}); }); @@ -281,15 +288,15 @@ describe('Auth Controller', () => { }); }); - it('should unset the kuzzle.jwt property', () => { + it('should unset the authenticationToken property', () => { return kuzzle.auth.logout() .then(() => { - should(kuzzle.jwt).be.undefined(); + should(kuzzle.auth.authenticationToken).be.null(); }); }); }); - describe('updateMyCredentials', () => { + describe('#updateMyCredentials', () => { it('should call auth/updateMyCredentials query with the user credentials and return a Promise which resolves a json object', () => { const credentials = {foo: 'bar'}; @@ -316,7 +323,7 @@ describe('Auth Controller', () => { }); }); - describe('updateSelf', () => { + describe('#updateSelf', () => { it('should call auth/updateSelf query with the user content and return a Promise which resolves a User object', () => { const body = {foo: 'bar'}; @@ -344,7 +351,7 @@ describe('Auth Controller', () => { }); }); - describe('validateMyCredentials', () => { + describe('#validateMyCredentials', () => { it('should call auth/validateMyCredentials query with the strategy and its credentials and return a Promise which resolves a boolean', () => { const body = {foo: 'bar'}; @@ -366,11 +373,11 @@ describe('Auth Controller', () => { }); }); - describe('refreshToken', () => { - const tokenResponse = { _id: 'foo', jwt: 'newToken' }; + describe('#refreshToken', () => { + const tokenResponse = { _id: 'foo', jwt: generateJwt() }; beforeEach(() => { - kuzzle.jwt = 'jwt'; + kuzzle.auth.jwt = generateJwt(); kuzzle.query.resolves({result: tokenResponse}); }); @@ -386,7 +393,8 @@ describe('Auth Controller', () => { }); should(res).be.eql(tokenResponse); - should(kuzzle.jwt).be.eql('newToken'); + should(kuzzle.auth.authenticationToken).not.be.null(); + should(kuzzle.auth.authenticationToken).be.instanceOf(Jwt); }); }); @@ -402,8 +410,41 @@ describe('Auth Controller', () => { }); should(res).be.eql(tokenResponse); - should(kuzzle.jwt).be.eql('newToken'); + should(kuzzle.auth.authenticationToken).not.be.null(); + should(kuzzle.auth.authenticationToken).be.instanceOf(Jwt); }); }); }); + + describe('#set authenticationToken', () => { + beforeEach(() => { + jwt = generateJwt(); + }); + + it('should unset the authenticationToken property if parameter is null', () => { + kuzzle.auth.authenticationToken = jwt; + kuzzle.auth.authenticationToken = null; + + should(kuzzle.auth.authenticationToken).be.null(); + }); + + it('should set the authenticationToken property if parameter is a string', () => { + kuzzle.auth.authenticationToken = generateJwt(); + + should(kuzzle.auth.authenticationToken).be.instanceOf(Jwt); + should(kuzzle.auth.authenticationToken.encodedJwt).be.eql(jwt); + }); + + it('should throw if parameter is not a string', () => { + kuzzle.auth.authenticationToken = jwt; + + should(function() { + kuzzle.auth.authenticationToken = 1234; + }).throw(); + + should(kuzzle.auth.authenticationToken).be.instanceOf(Jwt); + should(kuzzle.auth.authenticationToken.encodedJwt).be.eql(jwt); + }); + }); + }); diff --git a/test/core/Jwt.test.js b/test/core/Jwt.test.js new file mode 100644 index 000000000..f7a6366a3 --- /dev/null +++ b/test/core/Jwt.test.js @@ -0,0 +1,34 @@ +const + Jwt = require('../../src/core/Jwt'), + generateJwt = require('../mocks/generateJwt.mock'), + should = require('should'); + +describe('Jwt', () => { + let + authenticationToken; + + describe('#constructor', () => { + it('should construct a Jwt instance and decode the payload', () => { + const + expiresAt = Date.now() + 3600 * 1000, + encodedJwt = generateJwt('user-gordon', expiresAt); + + authenticationToken = new Jwt(encodedJwt); + + should(authenticationToken.encodedJwt).be.eql(encodedJwt); + should(authenticationToken.userId).be.eql('user-gordon'); + should(authenticationToken.expiresAt).be.eql(expiresAt); + should(authenticationToken.expired).be.eql(false); + }); + }); + + describe('#get expired', () => { + it('should return a boolean according to expiresAt', () => { + const encodedJwt = generateJwt('user-gordon', Date.now() - 1000); + + authenticationToken = new Jwt(encodedJwt); + + should(authenticationToken.expired).be.eql(true); + }); + }); +}); diff --git a/test/kuzzle/connect.test.js b/test/kuzzle/connect.test.js index fb27f93d7..91b16b573 100644 --- a/test/kuzzle/connect.test.js +++ b/test/kuzzle/connect.test.js @@ -2,6 +2,7 @@ const should = require('should'), sinon = require('sinon'), ProtocolMock = require('../mocks/protocol.mock'), + generateJwt = require('../mocks/generateJwt.mock'), Kuzzle = require('../../src/Kuzzle'); describe('Kuzzle connect', () => { @@ -85,36 +86,38 @@ describe('Kuzzle connect', () => { }); it('should keep a valid JWT at reconnection', () => { - const kuzzle = new Kuzzle(protocols.somewhereagain); + const + jwt = generateJwt(), + kuzzle = new Kuzzle(protocols.somewhereagain); kuzzle.auth.checkToken = sinon.stub().resolves({ valid: true }); - kuzzle.jwt = 'foobar'; + kuzzle.jwt = jwt; return kuzzle.connect() .then(() => { should(kuzzle.auth.checkToken).be.calledOnce(); - should(kuzzle.auth.checkToken).be.calledWith('foobar'); - should(kuzzle.jwt).be.eql('foobar'); + should(kuzzle.jwt).be.eql(jwt); }); }); it('should empty the JWT at reconnection if it has expired', () => { - const kuzzle = new Kuzzle(protocols.somewhereagain); + const + jwt = generateJwt(), + kuzzle = new Kuzzle(protocols.somewhereagain); kuzzle.auth.checkToken = sinon.stub().resolves({ valid: false }); - kuzzle.jwt = 'foobar'; + kuzzle.jwt = jwt; return kuzzle.connect() .then(() => { should(kuzzle.auth.checkToken).be.calledOnce(); - should(kuzzle.auth.checkToken).be.calledWith('foobar'); - should(kuzzle.jwt).be.undefined(); + should(kuzzle.jwt).be.null(); }); }); diff --git a/test/kuzzle/constructor.test.js b/test/kuzzle/constructor.test.js index 04a7a955c..0100f48d9 100644 --- a/test/kuzzle/constructor.test.js +++ b/test/kuzzle/constructor.test.js @@ -126,7 +126,7 @@ describe('Kuzzle constructor', () => { should(kuzzle.realtime).be.an.instanceof(RealTimeController); should(kuzzle.protocol).be.an.instanceof(ProtocolMock); should(kuzzle.sdkVersion).be.a.String().and.be.equal(version); - should(kuzzle.jwt).be.undefined(); + should(kuzzle.jwt).be.null(); }); it('should set autoQueue and autoReplay if offlineMode is set to auto', () => { diff --git a/test/kuzzle/getters.test.js b/test/kuzzle/getters.test.js index b40c84591..f6d54d04d 100644 --- a/test/kuzzle/getters.test.js +++ b/test/kuzzle/getters.test.js @@ -1,14 +1,22 @@ const should = require('should'), ProtocolMock = require('../mocks/protocol.mock'), + generateJwt = require('../mocks/generateJwt.mock'), Kuzzle = require('../../src/Kuzzle'); describe('Kuzzle getters', () => { - let kuzzle; + let + jwt, + kuzzle; beforeEach(() => { const protocol = new ProtocolMock('somewhere'); + kuzzle = new Kuzzle(protocol); + + jwt = generateJwt(); + + kuzzle.auth.authenticationToken = jwt; }); it('should get "autoQueue" property from private _autoQueue one', () => { @@ -26,9 +34,11 @@ describe('Kuzzle getters', () => { should(kuzzle.autoReplay).be.equal('foo-bar'); }); - it('should get "jwt" property from private _jwt one', () => { - kuzzle._jwt = 'foo-bar'; - should(kuzzle.jwt).be.equal('foo-bar'); + it('should get "jwt" property from auth authenticationToken', () => { + should(kuzzle.jwt).be.equal(kuzzle.auth.authenticationToken.encodedJwt); + + kuzzle.jwt = null; + should(kuzzle.jwt).be.equal(null); }); it('should get "host" property from protocol instance', () => { diff --git a/test/kuzzle/protocol.test.js b/test/kuzzle/protocol.test.js index c485a7ee2..1f702e185 100644 --- a/test/kuzzle/protocol.test.js +++ b/test/kuzzle/protocol.test.js @@ -2,6 +2,7 @@ const should = require('should'), sinon = require('sinon'), ProtocolMock = require('../mocks/protocol.mock'), + generateJwt = require('../mocks/generateJwt.mock'), Kuzzle = require('../../src/Kuzzle'); describe('Kuzzle protocol methods', () => { @@ -48,12 +49,12 @@ describe('Kuzzle protocol methods', () => { }); it('should empty the jwt when a "tokenExpired" events is triggered', () => { - kuzzle.jwt = 'foobar'; + kuzzle.jwt = generateJwt(); kuzzle.connect(); kuzzle.protocol.emit('tokenExpired'); - should(kuzzle.jwt).be.undefined(); + should(kuzzle.jwt).be.null(); }); }); }); diff --git a/test/kuzzle/query.test.js b/test/kuzzle/query.test.js index 27f611cdd..bbae94349 100644 --- a/test/kuzzle/query.test.js +++ b/test/kuzzle/query.test.js @@ -2,6 +2,7 @@ const should = require('should'), sinon = require('sinon'), ProtocolMock = require('../mocks/protocol.mock'), + generateJwt = require('../mocks/generateJwt.mock'), Kuzzle = require('../../src/Kuzzle'); describe('Kuzzle query management', () => { @@ -164,13 +165,14 @@ describe('Kuzzle query management', () => { }); it('should set jwt except for auth/checkToken', () => { - kuzzle.jwt = 'fake-token'; + const jwt = generateJwt(); + kuzzle.jwt = jwt; kuzzle.query({controller: 'foo', action: 'bar'}, {}); kuzzle.query({controller: 'auth', action: 'checkToken'}, {}); should(kuzzle.protocol.query).be.calledTwice(); - should(kuzzle.protocol.query.firstCall.args[0].jwt).be.exactly('fake-token'); + should(kuzzle.protocol.query.firstCall.args[0].jwt).be.exactly(jwt); should(kuzzle.protocol.query.secondCall.args[0].jwt).be.undefined(); }); diff --git a/test/kuzzle/setters.test.js b/test/kuzzle/setters.test.js index 070d56562..df52c74d4 100644 --- a/test/kuzzle/setters.test.js +++ b/test/kuzzle/setters.test.js @@ -2,6 +2,8 @@ const should = require('should'), sinon = require('sinon'), ProtocolMock = require('../mocks/protocol.mock'), + generateJwt = require('../mocks/generateJwt.mock'), + Jwt = require('../../src/core/Jwt'), Kuzzle = require('../../src/Kuzzle'); describe('Kuzzle setters', () => { @@ -67,38 +69,9 @@ describe('Kuzzle setters', () => { }); describe('#jwt', () => { - it('should unset the _jwt property if parameter is null', () => { - kuzzle._jwt = 'foo-bar'; - kuzzle.jwt = null; - should(kuzzle._jwt).be.undefined(); - }); - - it('should set the _jwt property if parameter is a string', () => { - kuzzle.jwt = 'foo-bar'; - should(kuzzle._jwt).be.equal('foo-bar'); - }); - - it('should set the _jwt property if parameter is an well formated object', () => { - kuzzle.jwt = {result: {jwt: 'foo-bar'}}; - should(kuzzle._jwt).be.equal('foo-bar'); - }); - - it('should throw if parameter is an bad formated object', () => { - kuzzle._jwt = 'old-jwt'; - - should(function() { - kuzzle.jwt = {foo: 'bar'}; - }).throw(); - should(kuzzle._jwt).be.equal('old-jwt'); - }); - - it('should throw if parameter is not a string', () => { - kuzzle._jwt = 'old-jwt'; - - should(function() { - kuzzle.jwt = 1234; - }).throw(); - should(kuzzle._jwt).be.equal('old-jwt'); + it('should set the auth controller authenticationToken property', () => { + kuzzle.jwt = generateJwt(); + should(kuzzle.auth.authenticationToken).be.instanceOf(Jwt); }); }); diff --git a/test/mocks/generateJwt.mock.js b/test/mocks/generateJwt.mock.js new file mode 100644 index 000000000..4c0feb5cf --- /dev/null +++ b/test/mocks/generateJwt.mock.js @@ -0,0 +1,14 @@ +function generateJwt (userId = 'test-user', expiresAt = null) { + const + header = { alg: 'HS256', typ: 'JWT' }, + payload = { _id: userId, iat: Date.now(), exp: expiresAt }, + signature = 'who care ?'; + + expiresAt = expiresAt || Date.now() + 3600 * 1000; + + return [ header, payload, signature] + .map(data => Buffer.from(JSON.stringify(data)).toString('base64')) + .join('.'); +} + +module.exports = generateJwt; \ No newline at end of file From 7e62b84c8604e5ad634b079bb98c0add8e377ded Mon Sep 17 00:00:00 2001 From: Aschen Date: Tue, 7 May 2019 12:02:22 +0200 Subject: [PATCH 08/12] add missing file --- src/core/Jwt.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/core/Jwt.js diff --git a/src/core/Jwt.js b/src/core/Jwt.js new file mode 100644 index 000000000..8df8bb31c --- /dev/null +++ b/src/core/Jwt.js @@ -0,0 +1,39 @@ +'use strict'; + +class Jwt { + constructor (encodedJwt) { + this._encodedJwt = encodedJwt; + + this._userId = null; + this._expiresAt = null; + + this._decode(); + } + + get encodedJwt () { + return this._encodedJwt; + } + + get userId () { + return this._userId; + } + + get expiresAt () { + return this._expiresAt; + } + + get expired () { + return Date.now() > this.expiresAt; + } + + _decode () { + const + [, payloadRaw, ] = this._encodedJwt.split('.'), + payload = JSON.parse(new Buffer(payloadRaw, 'base64').toString()); + + this._userId = payload._id; + this._expiresAt = payload.exp; + } +} + +module.exports = Jwt; \ No newline at end of file From 404aaf1020fe93532063bdecaacc23d0b4419a3f Mon Sep 17 00:00:00 2001 From: Aschen Date: Thu, 9 May 2019 17:36:26 +0200 Subject: [PATCH 09/12] fix tokenExpired event --- src/controllers/realtime/index.js | 4 ++-- test/controllers/realtime.test.js | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/controllers/realtime/index.js b/src/controllers/realtime/index.js index cd3145a22..fc0aabe07 100644 --- a/src/controllers/realtime/index.js +++ b/src/controllers/realtime/index.js @@ -111,10 +111,10 @@ class RealTimeController extends BaseController { } this.subscriptions = {}; - this.kuzzle.jwt = undefined; - this.kuzzle.jwtExpiresAt = undefined; + this.kuzzle.auth.authenticationToken = null; const now = Date.now(); + if ((now - this.lastExpirationTimestamp) > expirationThrottleDelay) { this.lastExpirationTimestamp = now; this.kuzzle.emit('tokenExpired'); diff --git a/test/controllers/realtime.test.js b/test/controllers/realtime.test.js index 6b8a4aaaa..eed9d635b 100644 --- a/test/controllers/realtime.test.js +++ b/test/controllers/realtime.test.js @@ -1,5 +1,7 @@ const + AuthController = require('../../src/controllers/auth'), RealtimeController = require('../../src/controllers/realtime'), + generateJwt = require('../mocks/generateJwt.mock'), mockrequire = require('mock-require'), sinon = require('sinon'), should = require('should'), @@ -15,6 +17,8 @@ describe('Realtime Controller', () => { emit: sinon.stub() }; kuzzle.realtime = new RealtimeController(kuzzle); + kuzzle.auth = new AuthController(kuzzle); + kuzzle.auth.authenticationToken = generateJwt(); }); after(() => { @@ -282,7 +286,7 @@ describe('Realtime Controller', () => { should(kuzzle.realtime.subscriptions).be.empty(); should(stub.callCount).be.eql(10); should(kuzzle.emit).calledOnce().calledWith('tokenExpired'); - should(kuzzle.jwt).be.undefined(); + should(kuzzle.auth.authenticationToken).be.null(); }); it('should throttle to prevent emitting duplicate occurrences of the same event', () => { From 36eff29584b3947c97f272557c35479cbf81a5b5 Mon Sep 17 00:00:00 2001 From: Adrien Maret Date: Fri, 17 May 2019 16:33:53 +0200 Subject: [PATCH 10/12] Update test/mocks/generateJwt.mock.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Sébastien Cottinet --- test/mocks/generateJwt.mock.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/mocks/generateJwt.mock.js b/test/mocks/generateJwt.mock.js index 4c0feb5cf..87cabfab1 100644 --- a/test/mocks/generateJwt.mock.js +++ b/test/mocks/generateJwt.mock.js @@ -2,7 +2,7 @@ function generateJwt (userId = 'test-user', expiresAt = null) { const header = { alg: 'HS256', typ: 'JWT' }, payload = { _id: userId, iat: Date.now(), exp: expiresAt }, - signature = 'who care ?'; + signature = 'who cares?'; expiresAt = expiresAt || Date.now() + 3600 * 1000; @@ -11,4 +11,4 @@ function generateJwt (userId = 'test-user', expiresAt = null) { .join('.'); } -module.exports = generateJwt; \ No newline at end of file +module.exports = generateJwt; From 205840eaea2d84207428da4f526aa141c0a6c413 Mon Sep 17 00:00:00 2001 From: Aschen Date: Fri, 17 May 2019 16:42:35 +0200 Subject: [PATCH 11/12] Throw error with invalid jwt --- src/controllers/auth.js | 2 +- src/core/Jwt.js | 13 +++++++++++-- test/core/Jwt.test.js | 12 ++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/controllers/auth.js b/src/controllers/auth.js index 47e954121..7480c0055 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -58,7 +58,7 @@ class AuthController extends BaseController { * @param {string} token The jwt token to check * @return {Promise|*|PromiseLike|Promise} */ - checkToken (token = undefined) { + checkToken (token) { if (token === undefined && this.authenticationToken) { token = this.authenticationToken.encodedJwt; } diff --git a/src/core/Jwt.js b/src/core/Jwt.js index 8df8bb31c..82b7e69d6 100644 --- a/src/core/Jwt.js +++ b/src/core/Jwt.js @@ -27,9 +27,18 @@ class Jwt { } _decode () { - const - [, payloadRaw, ] = this._encodedJwt.split('.'), + const [, payloadRaw, ] = this._encodedJwt.split('.'); + + if (!payloadRaw) { + throw new Error('Invalid JWT format'); + } + + let payload; + try { payload = JSON.parse(new Buffer(payloadRaw, 'base64').toString()); + } catch (error) { + throw new Error('Invalid JSON payload for JWT'); + } this._userId = payload._id; this._expiresAt = payload.exp; diff --git a/test/core/Jwt.test.js b/test/core/Jwt.test.js index f7a6366a3..4c9583c97 100644 --- a/test/core/Jwt.test.js +++ b/test/core/Jwt.test.js @@ -20,6 +20,18 @@ describe('Jwt', () => { should(authenticationToken.expiresAt).be.eql(expiresAt); should(authenticationToken.expired).be.eql(false); }); + + it('should throw with an invalid JWT format', () => { + should(() => { + new Jwt('this-is-invalid') + }).throwError('Invalid JWT format'); + }); + + it('should throw with an invalid JSON payload', () => { + should(() => { + new Jwt('this-is.not-json-payload.for-sure') + }).throwError('Invalid JSON payload for JWT'); + }); }); describe('#get expired', () => { From 28c869f8b6b5cf206f32e49255900d020eaba176 Mon Sep 17 00:00:00 2001 From: Aschen Date: Fri, 17 May 2019 17:44:58 +0200 Subject: [PATCH 12/12] fix tests --- src/core/Jwt.js | 12 ++++++++++-- test/core/Jwt.test.js | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/core/Jwt.js b/src/core/Jwt.js index 82b7e69d6..d3fff7a6a 100644 --- a/src/core/Jwt.js +++ b/src/core/Jwt.js @@ -1,5 +1,13 @@ 'use strict'; +const decodeBase64 = base64 => { + if (Buffer) { + return Buffer.from(base64, 'base64').toString(); + } + + return atob(base64); +}; + class Jwt { constructor (encodedJwt) { this._encodedJwt = encodedJwt; @@ -27,7 +35,7 @@ class Jwt { } _decode () { - const [, payloadRaw, ] = this._encodedJwt.split('.'); + const [, payloadRaw, ] = this._encodedJwt.split('.'); if (!payloadRaw) { throw new Error('Invalid JWT format'); @@ -35,7 +43,7 @@ class Jwt { let payload; try { - payload = JSON.parse(new Buffer(payloadRaw, 'base64').toString()); + payload = JSON.parse(decodeBase64(payloadRaw)); } catch (error) { throw new Error('Invalid JSON payload for JWT'); } diff --git a/test/core/Jwt.test.js b/test/core/Jwt.test.js index 4c9583c97..2243613e2 100644 --- a/test/core/Jwt.test.js +++ b/test/core/Jwt.test.js @@ -23,13 +23,13 @@ describe('Jwt', () => { it('should throw with an invalid JWT format', () => { should(() => { - new Jwt('this-is-invalid') + new Jwt('this-is-invalid'); }).throwError('Invalid JWT format'); }); it('should throw with an invalid JSON payload', () => { should(() => { - new Jwt('this-is.not-json-payload.for-sure') + new Jwt('this-is.not-json-payload.for-sure'); }).throwError('Invalid JSON payload for JWT'); }); });