From fdc08930febc29c056dd1cd76e97dc070233ef57 Mon Sep 17 00:00:00 2001 From: Aschen Date: Tue, 13 Jul 2021 10:25:27 +0200 Subject: [PATCH 1/6] Add authenticator function --- src/Kuzzle.ts | 122 +++++++++++++++++++++++++++++-------- src/protocols/WebSocket.ts | 6 +- 2 files changed, 98 insertions(+), 30 deletions(-) diff --git a/src/Kuzzle.ts b/src/Kuzzle.ts index 96451d2ad..d58256c7e 100644 --- a/src/Kuzzle.ts +++ b/src/Kuzzle.ts @@ -32,6 +32,8 @@ const events = [ 'offlineQueuePop', 'queryError', 'reconnected', + 'resubscribe', + 'reconnectionError', 'tokenExpired' ]; @@ -42,41 +44,46 @@ export class Kuzzle extends KuzzleEventEmitter { /** * Protocol used by the SDK to communicate with Kuzzle. */ - public protocol: any; + protocol: any; /** * If true, automatically renews all subscriptions on a reconnected event. */ - public autoResubscribe: boolean; + autoResubscribe: boolean; /** * Timeout before sending again a similar event. */ - public eventTimeout: number; + eventTimeout: number; /** * SDK version. */ - public sdkVersion: string; + sdkVersion: string; /** * SDK name (e.g: `js@7.4.2`). */ - public sdkName: string; + sdkName: string; /** * Common volatile data that will be sent to all future requests. */ - public volatile: JSONObject; + volatile: JSONObject; /** * Handle deprecation warning in development mode (hidden in production) */ - public deprecationHandler: Deprecation; - - public auth: AuthController; - public bulk: any; - public collection: CollectionController; - public document: DocumentController; - public index: IndexController; - public ms: any; - public realtime: RealtimeController; - public security: any; - public server: any; + deprecationHandler: Deprecation; + /** + * Authenticator function called after a reconnection if the SDK is no longer + * authenticated. + */ + authenticator: () => Promise = null; + + auth: AuthController; + bulk: any; + collection: CollectionController; + document: DocumentController; + index: IndexController; + ms: any; + realtime: RealtimeController; + security: any; + server: any; private _protectedEvents: any; private _offlineQueue: any; @@ -236,38 +243,38 @@ export class Kuzzle extends KuzzleEventEmitter { this._cookieAuthentication = typeof options.cookieAuth === 'boolean' ? options.cookieAuth : false; - + if (this._cookieAuthentication) { this.protocol.enableCookieSupport(); let autoQueueState; let autoReplayState; let autoResbuscribeState; - + this.protocol.addListener('websocketRenewalStart', () => { autoQueueState = this.autoQueue; autoReplayState = this.autoReplay; autoResbuscribeState = this.autoResubscribe; - + this.autoQueue = true; this.autoReplay = true; this.autoResubscribe = true; }); - + this.protocol.addListener('websocketRenewalDone', () => { this.autoQueue = autoQueueState; this.autoReplay = autoReplayState; this.autoResubscribe = autoResbuscribeState; }); } - + this.deprecationHandler = new Deprecation( typeof options.deprecationWarning === 'boolean' ? options.deprecationWarning : true ); - + if (this._cookieAuthentication && typeof XMLHttpRequest === 'undefined') { throw new Error('Support for cookie authentication with cookieAuth option is not supported outside a browser'); } - + // controllers this.useController(AuthController, 'auth'); this.useController(BulkController, 'bulk'); @@ -525,11 +532,19 @@ export class Kuzzle extends KuzzleEventEmitter { this.emit('disconnected', context); }); - this.protocol.addListener('reconnect', () => { + this.protocol.addListener('reconnect', async () => { if (this.autoQueue) { this.stopQueuing(); } + // If the SDK was authenticated, check if the token is still valid and try + // to re-authenticate if needed. Otherwise the SDK is in disconnected state. + if (this.jwt && ! await this.tryReAuthenticate()) { + this.disconnect(); + + return; + } + if (this.autoReplay) { this.playQueue(); } @@ -542,6 +557,59 @@ export class Kuzzle extends KuzzleEventEmitter { return this.protocol.connect(); } + /** + * Try to re-authenticate the SDK if the current token is invalid. + * + * If the token is invalid, this method will return false and emit a + * "reconnectionError" event when: + * - the SDK cannot re-authenticate using the authenticator function + * - the authenticator function is not set + */ + private async tryReAuthenticate (): Promise { + try { + const { valid } = await this.auth.checkToken(); + + if (! valid && this.authenticator) { + await this.authenticate(); + } + else if (! valid && ! this.authenticator) { + this.emit('reconnectionError', { + error: new Error('SDK is not authenticated after reconnection and no "authenticator" function is defined.') + }); + + return false; + } + } + catch (err) { + this.emit('reconnectionError', { + error: new Error(`Failed to authenticate the SDK after reconnection: ${err}`) + }); + + return false; + } + + return true; + } + + /** + * Use the "authenticator" function to authenticate the SDK. + * + * @returns The authentication token + */ + async authenticate (): Promise { + if (! this.authenticator) { + throw new Error('No "authenticator" function is defined.'); + } + + await this.authenticator(); + + if (! this.jwt) { + throw new Error('The "authenticator" function did not authenticate the SDK. ("jwt" is not set)'); + } + + return this.jwt; + } + /** * Adds a listener to a Kuzzle global event. When an event is fired, listeners are called in the order of their * insertion. @@ -801,7 +869,7 @@ Discarded request: ${JSON.stringify(request)}`)); uniqueQueue = {}, dequeuingProcess = () => { if (this.offlineQueue.length > 0) { - + this._timeoutRequest( this.offlineQueue[0].timeout, this.offlineQueue[0].request, @@ -853,7 +921,7 @@ Discarded request: ${JSON.stringify(request)}`)); /** * Sends a request with a timeout - * + * * @param delay Delay before the request is rejected if not resolved * @param request Request object * @param options Request options diff --git a/src/protocols/WebSocket.ts b/src/protocols/WebSocket.ts index 0ab395ca2..c08f13fe1 100644 --- a/src/protocols/WebSocket.ts +++ b/src/protocols/WebSocket.ts @@ -204,7 +204,7 @@ export default class WebSocketProtocol extends BaseProtocolRealtime { /** * In case you're running a Kuzzle version under 2.10.0 * The response from a browser custom ping will be another payload. - * We need to clear this timeout at each message to keep + * We need to clear this timeout at each message to keep * the connection alive if it's the case */ clearTimeout(this.pongTimeoutId); @@ -227,7 +227,7 @@ export default class WebSocketProtocol extends BaseProtocolRealtime { super.enableCookieSupport(); this._httpProtocol = new HttpProtocol( - this.host, + this.host, { port: this.port, ssl: this.ssl, @@ -279,7 +279,7 @@ export default class WebSocketProtocol extends BaseProtocolRealtime { }); }) .catch(error => this.emit(formattedRequest.payload.requestId, {error})); - + } /** From a01727fed749858f0e7fe5fcfeca1c73857b4ee7 Mon Sep 17 00:00:00 2001 From: Aschen Date: Tue, 13 Jul 2021 10:41:14 +0200 Subject: [PATCH 2/6] nit --- src/Kuzzle.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Kuzzle.ts b/src/Kuzzle.ts index d58256c7e..d1d7452b1 100644 --- a/src/Kuzzle.ts +++ b/src/Kuzzle.ts @@ -32,7 +32,6 @@ const events = [ 'offlineQueuePop', 'queryError', 'reconnected', - 'resubscribe', 'reconnectionError', 'tokenExpired' ]; @@ -597,8 +596,8 @@ export class Kuzzle extends KuzzleEventEmitter { * @returns The authentication token */ async authenticate (): Promise { - if (! this.authenticator) { - throw new Error('No "authenticator" function is defined.'); + if (typeof this.authenticator !== 'function') { + throw new Error('The "authenticator" property must be a function.'); } await this.authenticator(); From 838c1e967b51259bfafb0b5784f9626cd9200b7d Mon Sep 17 00:00:00 2001 From: Aschen Date: Tue, 13 Jul 2021 11:52:37 +0200 Subject: [PATCH 3/6] add doc and tests --- .../core-classes/kuzzle/authenticate/index.md | 24 +++ .../authenticate/snippets/authenticate.js | 14 ++ .../snippets/authenticate.test.yml | 8 + doc/7/core-classes/kuzzle/properties/index.md | 63 ++++--- doc/7/essentials/offline-tools/index.md | 31 ++++ test/kuzzle/authenticator.test.js | 169 ++++++++++++++++++ test/kuzzle/listenersManagement.test.js | 1 + 7 files changed, 287 insertions(+), 23 deletions(-) create mode 100644 doc/7/core-classes/kuzzle/authenticate/index.md create mode 100644 doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.js create mode 100644 doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.test.yml create mode 100644 test/kuzzle/authenticator.test.js diff --git a/doc/7/core-classes/kuzzle/authenticate/index.md b/doc/7/core-classes/kuzzle/authenticate/index.md new file mode 100644 index 000000000..f42327893 --- /dev/null +++ b/doc/7/core-classes/kuzzle/authenticate/index.md @@ -0,0 +1,24 @@ +--- +code: true +type: page +title: authenticate +description: Authenticate the SDK with the setted authenticator +--- + +# authenticate + +Authenticate the SDK by using the function set in the [authenticator](/sdk/js/7/core-classes/kuzzle/properties#authenticator) property. + +## Arguments + +```js +authenticate(); +``` + +## Resolves + +Resolves to the authentication token. + +## Usage + +<<< ./snippets/authenticate.js diff --git a/doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.js b/doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.js new file mode 100644 index 000000000..749dd3860 --- /dev/null +++ b/doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.js @@ -0,0 +1,14 @@ +kuzzle.authenticator = async () => { + await kuzzle.auth.login('local', { username: 'foo', password: 'bar' }); +}; + +try { + const jwt = await kuzzle.authenticate(); + console.log(jwt); + /* + 'eyJhbGciOiJIUzI1NiIsIkpXVCJ9.eyJfaWQiOiJmb28iLCJpYXQiOjE.wSPmb0z2tErRdYEg' + */ + console.log('Success'); +} catch (error) { + console.error(error.message); +} diff --git a/doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.test.yml b/doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.test.yml new file mode 100644 index 000000000..0c5300fe7 --- /dev/null +++ b/doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.test.yml @@ -0,0 +1,8 @@ +--- +name: kuzzle#authenticate +description: Authenticate the SDK +hooks: + before: curl -X POST kuzzle:7512/users/foo/_create -H "Content-Type:application/json" --data '{"content":{"profileIds":["default"]},"credentials":{"local":{"username":"foo","password":"bar"}}}' + after: curl -X DELETE kuzzle:7512/users/foo +template: default +expected: Success diff --git a/doc/7/core-classes/kuzzle/properties/index.md b/doc/7/core-classes/kuzzle/properties/index.md index 6e17e1451..53b8dc095 100644 --- a/doc/7/core-classes/kuzzle/properties/index.md +++ b/doc/7/core-classes/kuzzle/properties/index.md @@ -8,12 +8,12 @@ order: 10 # Read-only properties -| Property name | Type | Description | -| -------------------- | -------- | ---------------------| -| `authenticated` |
boolean
| Returns `true` if the SDK holds a valid token | -| `connected` |
boolean
| Returns `true` if the SDK is currently connected to a Kuzzle server. | -| `offlineQueue` |
object[]
| Contains the queued requests during offline mode | -| `protocol` |
Protocol
| Protocol used by the SDK | +| Property name | Type | Description | +|-----------------|---------------------|----------------------------------------------------------------------| +| `authenticated` |
boolean
| Returns `true` if the SDK holds a valid token | +| `connected` |
boolean
| Returns `true` if the SDK is currently connected to a Kuzzle server. | +| `offlineQueue` |
object[]
| Contains the queued requests during offline mode | +| `protocol` |
Protocol
| Protocol used by the SDK | ### connected @@ -24,18 +24,35 @@ See the associated documentation: # Writable properties -| Property name | Type | Description | -| -------------------- | -------- | ---------------------| -| `autoQueue` |
boolean
| If `true`, automatically queues all requests during offline mode | -| `autoReplay` |
boolean
| If `true`, automatically replays queued requests on a `reconnected` event | -| `autoResubscribe` |
boolean
| If `true`, automatically renews all subscriptions on a `reconnected` event | -| `jwt` |
string
| Authentication token | -| `offlineQueueLoader` |
function
| Called before dequeuing requests after exiting offline mode, to add items at the beginning of the offline queue | -| `queueFilter` |
function
| Custom function called during offline mode to filter queued requests on-the-fly | -| `queueMaxSize` |
number
| Number of maximum requests kept during offline mode| -| `queueTTL` |
number
| Time a queued request is kept during offline mode, in milliseconds | -| `replayInterval` |
number
| Delay between each replayed requests | -| `volatile` |
object
| Common volatile data, will be sent to all future requests | +| Property name | Type | Description | +|----------------------|---------------------|-----------------------------------------------------------------------------------------------------------------| +| `authenticator` |
function
| Authenticator function to authenticate the SDK. (After called, the `jwt` property of the SDK has to be set.) | +| `autoQueue` |
boolean
| If `true`, automatically queues all requests during offline mode | +| `autoReplay` |
boolean
| If `true`, automatically replays queued requests on a `reconnected` event | +| `autoResubscribe` |
boolean
| If `true`, automatically renews all subscriptions on a `reconnected` event | +| `jwt` |
string
| Authentication token | +| `offlineQueueLoader` |
function
| Called before dequeuing requests after exiting offline mode, to add items at the beginning of the offline queue | +| `queueFilter` |
function
| Custom function called during offline mode to filter queued requests on-the-fly | +| `queueMaxSize` |
number
| Number of maximum requests kept during offline mode | +| `queueTTL` |
number
| Time a queued request is kept during offline mode, in milliseconds | +| `replayInterval` |
number
| Delay between each replayed requests | +| `volatile` |
object
| Common volatile data, will be sent to all future requests | + +### authenticator + +The `authenticator` property can be set to a function returning a promise. + +This function will be called after a successful reconnection if the current authentication token is not valid anymore. + +This function has to authenticate the SDK (by setting the `jwt` property). It can be a call to [auth.login](/sdk/js/7/controllers/auth/login) for example. + +```js +kuzzle.authenticator = async () => { + await kuzzle.auth.login('local', { username: 'user', password: 'pass' }); +} +``` + +If the `authenticator` function fail, then the `reconnected` event is never emitted and a `reconnectionError` event is emitted. ### offlineQueueLoader @@ -49,11 +66,11 @@ Promise offlineQueueLoader() The returned (or resolved) array must contain objects, each with the following properties: -| Property | Type | Description | -|---|---|---| -| `query` |
object
| Object representing the request that is about to be sent to Kuzzle, following the [Kuzzle API](/core/2/guides/main-concepts/querying) format | -| `reject` |
function
| A [Promise.reject](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject) function | -| `resolve` |
function
| A [Promise.resolve](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve) function | +| Property | Type | Description | +|-----------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------| +| `query` |
object
| Object representing the request that is about to be sent to Kuzzle, following the [Kuzzle API](/core/2/guides/main-concepts/querying) format | +| `reject` |
function
| A [Promise.reject](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject) function | +| `resolve` |
function
| A [Promise.resolve](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve) function | ### queueFilter diff --git a/doc/7/essentials/offline-tools/index.md b/doc/7/essentials/offline-tools/index.md index 2caace126..564be8adb 100644 --- a/doc/7/essentials/offline-tools/index.md +++ b/doc/7/essentials/offline-tools/index.md @@ -11,6 +11,33 @@ order: 400 The Kuzzle SDK provides a set of properties that helps your application to be resilient to the loss of network connection during its lifespan. +## Authentication after reconnection + +When the SDK reconnect, the authentication token may not be valid anymore. + +It's possible to set the [authenticator](/sdk/js/7/core-classes/kuzzle/properties#authenticator) function to allows the SDK to re-authenticate after a successful reconnection. + +
Example to automatically re-authenticate on reconnection + +```js +const { Kuzzle, WebSocket } = require('kuzzle'); + +const kuzzle = new Kuzzle(new WebSocket('localhost'), { autoResubscribe: true }); + +kuzzle.authenticator = async () => { + await kuzzle.auth.login('local', { username: 'test', password: 'test' }); +}; + +await kuzzle.connect(); +await kuzzle.authenticate(); + +await kuzzle.realtime.subscribe('test', 'test', {}, () => { + console.log('Received'); +}); +``` + +
+ ## Contructor options and properties These properties can be set in the `options` object when [instantiating a new SDK](/sdk/js/7/core-classes/kuzzle/constructor#arguments). @@ -77,6 +104,10 @@ A read-only `number` specifying the time in milliseconds between different recon Default value: *Depends on the Protocol* +### reconnectionError + +Emitted when the SDK reconnect to Kuzzle and does not have a valid authentication token or can't renew it with the [authenticator](/sdk/js/7/core-classes/kuzzle/properties#authenticator) function. + ## Methods ### [flushQueue()](/sdk/js/7/core-classes/kuzzle/flush-queue) diff --git a/test/kuzzle/authenticator.test.js b/test/kuzzle/authenticator.test.js new file mode 100644 index 000000000..164824e2b --- /dev/null +++ b/test/kuzzle/authenticator.test.js @@ -0,0 +1,169 @@ +const should = require('should'); +const sinon = require('sinon'); +const ProtocolMock = require('../mocks/protocol.mock'); +const { Kuzzle } = require('../../src/Kuzzle'); + +describe('Kuzzle authenticator function mecanisms', () => { + let kuzzle; + let protocol; + let validJwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + + beforeEach(() => { + protocol = new ProtocolMock('somewhere'); + kuzzle = new Kuzzle(protocol); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('reconnect listener', () => { + let reconnectedSpy; + let resolve; + let promise; + + beforeEach(() => { + kuzzle.jwt = validJwt; + + sinon.stub(kuzzle, 'tryReAuthenticate').resolves(true); + sinon.stub(kuzzle, 'disconnect'); + + reconnectedSpy = sinon.stub(); + kuzzle.on('reconnected', reconnectedSpy); + }); + + it('should try to re-authenticate when reconnecting if a JWT was set', async () => { + promise = new Promise(_resolve => { + resolve = _resolve; + }); + await kuzzle.connect(); + + protocol.emit('reconnect'); + + // We need a timeout since the listener on "reconnect" even is async + setTimeout(() => { + should(kuzzle.tryReAuthenticate).be.calledOnce(); + should(reconnectedSpy).be.calledOnce(); + resolve(); + }, 1); + + return promise; + }); + + it('should not fire the reconnected event and disconnect the SDK if authentication fail', async () => { + promise = new Promise(_resolve => { + resolve = _resolve; + }); + await kuzzle.connect(); + kuzzle.tryReAuthenticate.resolves(false); + + protocol.emit('reconnect'); + + // We need a timeout since the listener on "reconnect" even is async + setTimeout(() => { + should(kuzzle.tryReAuthenticate).be.calledOnce(); + should(kuzzle.disconnect).be.calledOnce(); + resolve(); + }, 1); + + return promise; + }); + + it('should not try to authenticate if the SDK was not authenticated', async () => { + promise = new Promise(_resolve => { + resolve = _resolve; + }); + await kuzzle.connect(); + kuzzle.jwt = null; + + protocol.emit('reconnect'); + + // We need a timeout since the listener on "reconnect" even is async + setTimeout(() => { + should(reconnectedSpy).be.calledOnce(); + resolve(); + }, 1); + + return promise; + }); + }); + + describe('#tryReAuthenticate', () => { + let reconnectionErrorSpy; + + beforeEach(() => { + sinon.stub(kuzzle.auth, 'checkToken').resolves({ valid: false }); + sinon.stub(kuzzle, 'authenticate').resolves(); + + reconnectionErrorSpy = sinon.stub(); + kuzzle.on('reconnectionError', reconnectionErrorSpy); + + kuzzle.authenticator = () => {}; + }); + + it('should returns true if the token is still valid', async () => { + kuzzle.auth.checkToken.resolves({ valid: true }); + + const ret = await kuzzle.tryReAuthenticate(); + + should(ret).be.true(); + should(kuzzle.authenticate).not.be.called(); + should(reconnectionErrorSpy).not.be.called(); + }); + + it('should call "authenticate" if the token is not valid', async () => { + const ret = await kuzzle.tryReAuthenticate(); + + should(ret).be.true(); + should(kuzzle.authenticate).be.calledOnce(); + should(reconnectionErrorSpy).not.be.called(); + }); + + it('should emit "reconnectionError" if the token is not valid and no "authenticator" is set', async () => { + kuzzle.authenticator = null; + + const ret = await kuzzle.tryReAuthenticate(); + + should(ret).be.false(); + should(reconnectionErrorSpy).be.called(); + }); + + it('should emit "reconnectionError" if the "authenticator" function fail', async () => { + kuzzle.authenticate.rejects('auth fail'); + + const ret = await kuzzle.tryReAuthenticate(); + + should(ret).be.false(); + should(reconnectionErrorSpy).be.called(); + }); + }); + + describe('#authenticate', () => { + beforeEach(() => { + kuzzle.authenticator = async () => { + kuzzle.jwt = validJwt; + }; + + sinon.spy(kuzzle, 'authenticator'); + }); + + it('should execute the "authenticator"', async () => { + const token = await kuzzle.authenticate(); + + should(kuzzle.authenticator).be.calledOnce(); + should(token).be.eql(validJwt); + }); + + it('should throw an error if the "authenticator" is not set', () => { + kuzzle.authenticator = null; + + should(kuzzle.authenticate()).be.rejected(); + }); + + it('should throw an error if the "authenticator" does not set the JWT', () => { + kuzzle.authenticator = async () => {}; + + should(kuzzle.authenticate()).be.rejected(); + }); + }); +}); \ No newline at end of file diff --git a/test/kuzzle/listenersManagement.test.js b/test/kuzzle/listenersManagement.test.js index a6d599315..9d776442c 100644 --- a/test/kuzzle/listenersManagement.test.js +++ b/test/kuzzle/listenersManagement.test.js @@ -30,6 +30,7 @@ describe('Kuzzle listeners management', () => { 'offlineQueuePop', 'queryError', 'reconnected', + 'reconnectionError', 'tokenExpired' ]; From d4f88e2859b828fd5be957250e5a13b1cc042139 Mon Sep 17 00:00:00 2001 From: Aschen Date: Tue, 13 Jul 2021 20:15:08 +0200 Subject: [PATCH 4/6] nit --- src/Kuzzle.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Kuzzle.ts b/src/Kuzzle.ts index d1d7452b1..e39521a31 100644 --- a/src/Kuzzle.ts +++ b/src/Kuzzle.ts @@ -563,21 +563,24 @@ export class Kuzzle extends KuzzleEventEmitter { * "reconnectionError" event when: * - the SDK cannot re-authenticate using the authenticator function * - the authenticator function is not set + * + * This method never returns a rejected promise. */ private async tryReAuthenticate (): Promise { try { const { valid } = await this.auth.checkToken(); - if (! valid && this.authenticator) { - await this.authenticate(); + if (valid) { + return true; } - else if (! valid && ! this.authenticator) { - this.emit('reconnectionError', { - error: new Error('SDK is not authenticated after reconnection and no "authenticator" function is defined.') - }); - return false; + if (! this.authenticator) { + throw new Error('No "authenticator" function is defined.'); } + + await this.authenticate(); + + return true; } catch (err) { this.emit('reconnectionError', { @@ -586,8 +589,6 @@ export class Kuzzle extends KuzzleEventEmitter { return false; } - - return true; } /** From ac15fc7f45a29748772ed16202881e3accb62c0d Mon Sep 17 00:00:00 2001 From: Aschen Date: Tue, 13 Jul 2021 20:17:27 +0200 Subject: [PATCH 5/6] nit --- src/Kuzzle.ts | 4 ---- test/kuzzle/authenticator.test.js | 9 --------- 2 files changed, 13 deletions(-) diff --git a/src/Kuzzle.ts b/src/Kuzzle.ts index e39521a31..a88b6546f 100644 --- a/src/Kuzzle.ts +++ b/src/Kuzzle.ts @@ -574,10 +574,6 @@ export class Kuzzle extends KuzzleEventEmitter { return true; } - if (! this.authenticator) { - throw new Error('No "authenticator" function is defined.'); - } - await this.authenticate(); return true; diff --git a/test/kuzzle/authenticator.test.js b/test/kuzzle/authenticator.test.js index 164824e2b..8a3a2d15c 100644 --- a/test/kuzzle/authenticator.test.js +++ b/test/kuzzle/authenticator.test.js @@ -119,15 +119,6 @@ describe('Kuzzle authenticator function mecanisms', () => { should(reconnectionErrorSpy).not.be.called(); }); - it('should emit "reconnectionError" if the token is not valid and no "authenticator" is set', async () => { - kuzzle.authenticator = null; - - const ret = await kuzzle.tryReAuthenticate(); - - should(ret).be.false(); - should(reconnectionErrorSpy).be.called(); - }); - it('should emit "reconnectionError" if the "authenticator" function fail', async () => { kuzzle.authenticate.rejects('auth fail'); From 95473600bd93084a29be9a585759b57c01bfc7c3 Mon Sep 17 00:00:00 2001 From: Aschen Date: Fri, 16 Jul 2021 15:24:25 +0200 Subject: [PATCH 6/6] Only re-auth if authenticator is set --- .../core-classes/kuzzle/authenticate/index.md | 4 ---- .../authenticate/snippets/authenticate.js | 7 ++---- doc/7/core-classes/kuzzle/properties/index.md | 4 ++-- src/Kuzzle.ts | 14 +++++------ test/kuzzle/authenticator.test.js | 23 +++++++++---------- 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/doc/7/core-classes/kuzzle/authenticate/index.md b/doc/7/core-classes/kuzzle/authenticate/index.md index f42327893..530f3dfb8 100644 --- a/doc/7/core-classes/kuzzle/authenticate/index.md +++ b/doc/7/core-classes/kuzzle/authenticate/index.md @@ -15,10 +15,6 @@ Authenticate the SDK by using the function set in the [authenticator](/sdk/js/7/ authenticate(); ``` -## Resolves - -Resolves to the authentication token. - ## Usage <<< ./snippets/authenticate.js diff --git a/doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.js b/doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.js index 749dd3860..d3d44ee1b 100644 --- a/doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.js +++ b/doc/7/core-classes/kuzzle/authenticate/snippets/authenticate.js @@ -3,11 +3,8 @@ kuzzle.authenticator = async () => { }; try { - const jwt = await kuzzle.authenticate(); - console.log(jwt); - /* - 'eyJhbGciOiJIUzI1NiIsIkpXVCJ9.eyJfaWQiOiJmb28iLCJpYXQiOjE.wSPmb0z2tErRdYEg' - */ + await kuzzle.authenticate(); + console.log('Success'); } catch (error) { console.error(error.message); diff --git a/doc/7/core-classes/kuzzle/properties/index.md b/doc/7/core-classes/kuzzle/properties/index.md index 53b8dc095..ff1437e70 100644 --- a/doc/7/core-classes/kuzzle/properties/index.md +++ b/doc/7/core-classes/kuzzle/properties/index.md @@ -44,7 +44,7 @@ The `authenticator` property can be set to a function returning a promise. This function will be called after a successful reconnection if the current authentication token is not valid anymore. -This function has to authenticate the SDK (by setting the `jwt` property). It can be a call to [auth.login](/sdk/js/7/controllers/auth/login) for example. +This function has to authenticate the SDK. It can be a call to [auth.login](/sdk/js/7/controllers/auth/login) for example. ```js kuzzle.authenticator = async () => { @@ -52,7 +52,7 @@ kuzzle.authenticator = async () => { } ``` -If the `authenticator` function fail, then the `reconnected` event is never emitted and a `reconnectionError` event is emitted. +If the `authenticator` function fail to authenticate the SDK, then the `reconnected` event is never emitted and a `reconnectionError` event is emitted. ### offlineQueueLoader diff --git a/src/Kuzzle.ts b/src/Kuzzle.ts index a88b6546f..f15f5b968 100644 --- a/src/Kuzzle.ts +++ b/src/Kuzzle.ts @@ -536,9 +536,9 @@ export class Kuzzle extends KuzzleEventEmitter { this.stopQueuing(); } - // If the SDK was authenticated, check if the token is still valid and try + // If an authenticator was set, check if the token is still valid and try // to re-authenticate if needed. Otherwise the SDK is in disconnected state. - if (this.jwt && ! await this.tryReAuthenticate()) { + if (this.authenticator && ! await this.tryReAuthenticate()) { this.disconnect(); return; @@ -592,18 +592,18 @@ export class Kuzzle extends KuzzleEventEmitter { * * @returns The authentication token */ - async authenticate (): Promise { + async authenticate (): Promise { if (typeof this.authenticator !== 'function') { throw new Error('The "authenticator" property must be a function.'); } await this.authenticator(); - if (! this.jwt) { - throw new Error('The "authenticator" function did not authenticate the SDK. ("jwt" is not set)'); - } + const { valid } = await this.auth.checkToken(); - return this.jwt; + if (! valid) { + throw new Error('The "authenticator" function failed to authenticate the SDK.'); + } } /** diff --git a/test/kuzzle/authenticator.test.js b/test/kuzzle/authenticator.test.js index 8a3a2d15c..c7b620da6 100644 --- a/test/kuzzle/authenticator.test.js +++ b/test/kuzzle/authenticator.test.js @@ -6,7 +6,6 @@ const { Kuzzle } = require('../../src/Kuzzle'); describe('Kuzzle authenticator function mecanisms', () => { let kuzzle; let protocol; - let validJwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; beforeEach(() => { protocol = new ProtocolMock('somewhere'); @@ -23,7 +22,7 @@ describe('Kuzzle authenticator function mecanisms', () => { let promise; beforeEach(() => { - kuzzle.jwt = validJwt; + kuzzle.authenticator = async () => {}; sinon.stub(kuzzle, 'tryReAuthenticate').resolves(true); sinon.stub(kuzzle, 'disconnect'); @@ -32,7 +31,7 @@ describe('Kuzzle authenticator function mecanisms', () => { kuzzle.on('reconnected', reconnectedSpy); }); - it('should try to re-authenticate when reconnecting if a JWT was set', async () => { + it('should try to re-authenticate when reconnecting if an authenticator was set', async () => { promise = new Promise(_resolve => { resolve = _resolve; }); @@ -69,12 +68,12 @@ describe('Kuzzle authenticator function mecanisms', () => { return promise; }); - it('should not try to authenticate if the SDK was not authenticated', async () => { + it('should not try to authenticate if no authenticator was set', async () => { + kuzzle.authenticator = null; promise = new Promise(_resolve => { resolve = _resolve; }); await kuzzle.connect(); - kuzzle.jwt = null; protocol.emit('reconnect'); @@ -131,18 +130,18 @@ describe('Kuzzle authenticator function mecanisms', () => { describe('#authenticate', () => { beforeEach(() => { - kuzzle.authenticator = async () => { - kuzzle.jwt = validJwt; - }; + kuzzle.authenticator = async () => {}; + + sinon.stub(kuzzle.auth, 'checkToken').resolves({ valid: true }); sinon.spy(kuzzle, 'authenticator'); }); it('should execute the "authenticator"', async () => { - const token = await kuzzle.authenticate(); + await kuzzle.authenticate(); should(kuzzle.authenticator).be.calledOnce(); - should(token).be.eql(validJwt); + should(kuzzle.auth.checkToken).be.calledOnce(); }); it('should throw an error if the "authenticator" is not set', () => { @@ -151,8 +150,8 @@ describe('Kuzzle authenticator function mecanisms', () => { should(kuzzle.authenticate()).be.rejected(); }); - it('should throw an error if the "authenticator" does not set the JWT', () => { - kuzzle.authenticator = async () => {}; + it('should throw an error if the "authenticator" does not authenticate the SDK', () => { + kuzzle.auth.checkToken.resolves({ valid: false }); should(kuzzle.authenticate()).be.rejected(); });