Skip to content

Commit 2268913

Browse files
authored
Add authenticator function used at reconnection (#650)
When the SDK reconnect to Kuzzle, it trigger the `reconnected` event. The Realtime controller will try to resubscribe when this event is triggered. If the token had expired, then the Realtime controller will try to resubscribe with no authentication and thus the subscriptions request may fail. This PR include a new `authenticator` property, this property should contain a function that authenticate the SDK (with `auth.login` for example). The SDK will call the function before emitting the reconnected event, if the SDK was authenticated and cannot re-authenticate then the `reconnected` event will not be emitted and the SDK will be in the `disconnected` state. A new `reconnectionError` has been added and is triggered when the reconnection has failed
1 parent e7cdcaa commit 2268913

File tree

8 files changed

+361
-50
lines changed

8 files changed

+361
-50
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
code: true
3+
type: page
4+
title: authenticate
5+
description: Authenticate the SDK with the setted authenticator
6+
---
7+
8+
# authenticate
9+
10+
Authenticate the SDK by using the function set in the [authenticator](/sdk/js/7/core-classes/kuzzle/properties#authenticator) property.
11+
12+
## Arguments
13+
14+
```js
15+
authenticate();
16+
```
17+
18+
## Usage
19+
20+
<<< ./snippets/authenticate.js
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
kuzzle.authenticator = async () => {
2+
await kuzzle.auth.login('local', { username: 'foo', password: 'bar' });
3+
};
4+
5+
try {
6+
await kuzzle.authenticate();
7+
8+
console.log('Success');
9+
} catch (error) {
10+
console.error(error.message);
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
name: kuzzle#authenticate
3+
description: Authenticate the SDK
4+
hooks:
5+
before: curl -X POST kuzzle:7512/users/foo/_create -H "Content-Type:application/json" --data '{"content":{"profileIds":["default"]},"credentials":{"local":{"username":"foo","password":"bar"}}}'
6+
after: curl -X DELETE kuzzle:7512/users/foo
7+
template: default
8+
expected: Success

doc/7/core-classes/kuzzle/properties/index.md

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ order: 10
88

99
# Read-only properties
1010

11-
| Property name | Type | Description |
12-
| -------------------- | -------- | ---------------------|
13-
| `authenticated` | <pre>boolean</pre> | Returns `true` if the SDK holds a valid token |
14-
| `connected` | <pre>boolean</pre> | Returns `true` if the SDK is currently connected to a Kuzzle server. |
15-
| `offlineQueue` | <pre>object[]</pre> | Contains the queued requests during offline mode |
16-
| `protocol` | <pre>Protocol</pre> | Protocol used by the SDK |
11+
| Property name | Type | Description |
12+
|-----------------|---------------------|----------------------------------------------------------------------|
13+
| `authenticated` | <pre>boolean</pre> | Returns `true` if the SDK holds a valid token |
14+
| `connected` | <pre>boolean</pre> | Returns `true` if the SDK is currently connected to a Kuzzle server. |
15+
| `offlineQueue` | <pre>object[]</pre> | Contains the queued requests during offline mode |
16+
| `protocol` | <pre>Protocol</pre> | Protocol used by the SDK |
1717

1818
### connected
1919

@@ -24,18 +24,35 @@ See the associated documentation:
2424

2525
# Writable properties
2626

27-
| Property name | Type | Description |
28-
| -------------------- | -------- | ---------------------|
29-
| `autoQueue` | <pre>boolean</pre> | If `true`, automatically queues all requests during offline mode |
30-
| `autoReplay` | <pre>boolean</pre> | If `true`, automatically replays queued requests on a `reconnected` event |
31-
| `autoResubscribe` | <pre>boolean</pre> | If `true`, automatically renews all subscriptions on a `reconnected` event |
32-
| `jwt` | <pre>string</pre> | Authentication token |
33-
| `offlineQueueLoader` | <pre>function</pre> | Called before dequeuing requests after exiting offline mode, to add items at the beginning of the offline queue |
34-
| `queueFilter` | <pre>function</pre> | Custom function called during offline mode to filter queued requests on-the-fly |
35-
| `queueMaxSize` | <pre>number</pre> | Number of maximum requests kept during offline mode|
36-
| `queueTTL` | <pre>number</pre> | Time a queued request is kept during offline mode, in milliseconds |
37-
| `replayInterval` | <pre>number</pre> | Delay between each replayed requests |
38-
| `volatile` | <pre>object</pre> | Common volatile data, will be sent to all future requests |
27+
| Property name | Type | Description |
28+
|----------------------|---------------------|-----------------------------------------------------------------------------------------------------------------|
29+
| `authenticator` | <pre>function</pre> | Authenticator function to authenticate the SDK. (After called, the `jwt` property of the SDK has to be set.) |
30+
| `autoQueue` | <pre>boolean</pre> | If `true`, automatically queues all requests during offline mode |
31+
| `autoReplay` | <pre>boolean</pre> | If `true`, automatically replays queued requests on a `reconnected` event |
32+
| `autoResubscribe` | <pre>boolean</pre> | If `true`, automatically renews all subscriptions on a `reconnected` event |
33+
| `jwt` | <pre>string</pre> | Authentication token |
34+
| `offlineQueueLoader` | <pre>function</pre> | Called before dequeuing requests after exiting offline mode, to add items at the beginning of the offline queue |
35+
| `queueFilter` | <pre>function</pre> | Custom function called during offline mode to filter queued requests on-the-fly |
36+
| `queueMaxSize` | <pre>number</pre> | Number of maximum requests kept during offline mode |
37+
| `queueTTL` | <pre>number</pre> | Time a queued request is kept during offline mode, in milliseconds |
38+
| `replayInterval` | <pre>number</pre> | Delay between each replayed requests |
39+
| `volatile` | <pre>object</pre> | Common volatile data, will be sent to all future requests |
40+
41+
### authenticator
42+
43+
The `authenticator` property can be set to a function returning a promise.
44+
45+
This function will be called after a successful reconnection if the current authentication token is not valid anymore.
46+
47+
This function has to authenticate the SDK. It can be a call to [auth.login](/sdk/js/7/controllers/auth/login) for example.
48+
49+
```js
50+
kuzzle.authenticator = async () => {
51+
await kuzzle.auth.login('local', { username: 'user', password: 'pass' });
52+
}
53+
```
54+
55+
If the `authenticator` function fail to authenticate the SDK, then the `reconnected` event is never emitted and a `reconnectionError` event is emitted.
3956

4057
### offlineQueueLoader
4158

@@ -49,11 +66,11 @@ Promise<Object[]> offlineQueueLoader()
4966

5067
The returned (or resolved) array must contain objects, each with the following properties:
5168

52-
| Property | Type | Description |
53-
|---|---|---|
54-
| `query` | <pre>object</pre> | Object representing the request that is about to be sent to Kuzzle, following the [Kuzzle API](/core/2/guides/main-concepts/querying) format |
55-
| `reject` | <pre>function</pre> | A [Promise.reject](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject) function |
56-
| `resolve` | <pre>function</pre> | A [Promise.resolve](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve) function |
69+
| Property | Type | Description |
70+
|-----------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
71+
| `query` | <pre>object</pre> | Object representing the request that is about to be sent to Kuzzle, following the [Kuzzle API](/core/2/guides/main-concepts/querying) format |
72+
| `reject` | <pre>function</pre> | A [Promise.reject](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject) function |
73+
| `resolve` | <pre>function</pre> | A [Promise.resolve](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve) function |
5774

5875
### queueFilter
5976

doc/7/essentials/offline-tools/index.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,33 @@ order: 400
1111
The Kuzzle SDK provides a set of properties that helps your application to be resilient to the loss of network connection
1212
during its lifespan.
1313

14+
## Authentication after reconnection
15+
16+
When the SDK reconnect, the authentication token may not be valid anymore.
17+
18+
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.
19+
20+
<details><summary>Example to automatically re-authenticate on reconnection</summary>
21+
22+
```js
23+
const { Kuzzle, WebSocket } = require('kuzzle');
24+
25+
const kuzzle = new Kuzzle(new WebSocket('localhost'), { autoResubscribe: true });
26+
27+
kuzzle.authenticator = async () => {
28+
await kuzzle.auth.login('local', { username: 'test', password: 'test' });
29+
};
30+
31+
await kuzzle.connect();
32+
await kuzzle.authenticate();
33+
34+
await kuzzle.realtime.subscribe('test', 'test', {}, () => {
35+
console.log('Received');
36+
});
37+
```
38+
39+
</details>
40+
1441
## Contructor options and properties
1542

1643
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
77104

78105
Default value: *Depends on the Protocol*
79106

107+
### reconnectionError
108+
109+
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.
110+
80111
## Methods
81112

82113
### [flushQueue()](/sdk/js/7/core-classes/kuzzle/flush-queue)

src/Kuzzle.ts

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const events = [
3232
'offlineQueuePop',
3333
'queryError',
3434
'reconnected',
35+
'reconnectionError',
3536
'tokenExpired'
3637
];
3738

@@ -42,41 +43,46 @@ export class Kuzzle extends KuzzleEventEmitter {
4243
/**
4344
* Protocol used by the SDK to communicate with Kuzzle.
4445
*/
45-
public protocol: any;
46+
protocol: any;
4647
/**
4748
* If true, automatically renews all subscriptions on a reconnected event.
4849
*/
49-
public autoResubscribe: boolean;
50+
autoResubscribe: boolean;
5051
/**
5152
* Timeout before sending again a similar event.
5253
*/
53-
public eventTimeout: number;
54+
eventTimeout: number;
5455
/**
5556
* SDK version.
5657
*/
57-
public sdkVersion: string;
58+
sdkVersion: string;
5859
/**
5960
* SDK name (e.g: `[email protected]`).
6061
*/
61-
public sdkName: string;
62+
sdkName: string;
6263
/**
6364
* Common volatile data that will be sent to all future requests.
6465
*/
65-
public volatile: JSONObject;
66+
volatile: JSONObject;
6667
/**
6768
* Handle deprecation warning in development mode (hidden in production)
6869
*/
69-
public deprecationHandler: Deprecation;
70-
71-
public auth: AuthController;
72-
public bulk: any;
73-
public collection: CollectionController;
74-
public document: DocumentController;
75-
public index: IndexController;
76-
public ms: any;
77-
public realtime: RealtimeController;
78-
public security: any;
79-
public server: any;
70+
deprecationHandler: Deprecation;
71+
/**
72+
* Authenticator function called after a reconnection if the SDK is no longer
73+
* authenticated.
74+
*/
75+
authenticator: () => Promise<void> = null;
76+
77+
auth: AuthController;
78+
bulk: any;
79+
collection: CollectionController;
80+
document: DocumentController;
81+
index: IndexController;
82+
ms: any;
83+
realtime: RealtimeController;
84+
security: any;
85+
server: any;
8086

8187
private _protectedEvents: any;
8288
private _offlineQueue: any;
@@ -236,38 +242,38 @@ export class Kuzzle extends KuzzleEventEmitter {
236242
this._cookieAuthentication = typeof options.cookieAuth === 'boolean'
237243
? options.cookieAuth
238244
: false;
239-
245+
240246
if (this._cookieAuthentication) {
241247
this.protocol.enableCookieSupport();
242248
let autoQueueState;
243249
let autoReplayState;
244250
let autoResbuscribeState;
245-
251+
246252
this.protocol.addListener('websocketRenewalStart', () => {
247253
autoQueueState = this.autoQueue;
248254
autoReplayState = this.autoReplay;
249255
autoResbuscribeState = this.autoResubscribe;
250-
256+
251257
this.autoQueue = true;
252258
this.autoReplay = true;
253259
this.autoResubscribe = true;
254260
});
255-
261+
256262
this.protocol.addListener('websocketRenewalDone', () => {
257263
this.autoQueue = autoQueueState;
258264
this.autoReplay = autoReplayState;
259265
this.autoResubscribe = autoResbuscribeState;
260266
});
261267
}
262-
268+
263269
this.deprecationHandler = new Deprecation(
264270
typeof options.deprecationWarning === 'boolean' ? options.deprecationWarning : true
265271
);
266-
272+
267273
if (this._cookieAuthentication && typeof XMLHttpRequest === 'undefined') {
268274
throw new Error('Support for cookie authentication with cookieAuth option is not supported outside a browser');
269275
}
270-
276+
271277
// controllers
272278
this.useController(AuthController, 'auth');
273279
this.useController(BulkController, 'bulk');
@@ -525,11 +531,19 @@ export class Kuzzle extends KuzzleEventEmitter {
525531
this.emit('disconnected', context);
526532
});
527533

528-
this.protocol.addListener('reconnect', () => {
534+
this.protocol.addListener('reconnect', async () => {
529535
if (this.autoQueue) {
530536
this.stopQueuing();
531537
}
532538

539+
// If an authenticator was set, check if the token is still valid and try
540+
// to re-authenticate if needed. Otherwise the SDK is in disconnected state.
541+
if (this.authenticator && ! await this.tryReAuthenticate()) {
542+
this.disconnect();
543+
544+
return;
545+
}
546+
533547
if (this.autoReplay) {
534548
this.playQueue();
535549
}
@@ -542,6 +556,56 @@ export class Kuzzle extends KuzzleEventEmitter {
542556
return this.protocol.connect();
543557
}
544558

559+
/**
560+
* Try to re-authenticate the SDK if the current token is invalid.
561+
*
562+
* If the token is invalid, this method will return false and emit a
563+
* "reconnectionError" event when:
564+
* - the SDK cannot re-authenticate using the authenticator function
565+
* - the authenticator function is not set
566+
*
567+
* This method never returns a rejected promise.
568+
*/
569+
private async tryReAuthenticate (): Promise<boolean> {
570+
try {
571+
const { valid } = await this.auth.checkToken();
572+
573+
if (valid) {
574+
return true;
575+
}
576+
577+
await this.authenticate();
578+
579+
return true;
580+
}
581+
catch (err) {
582+
this.emit('reconnectionError', {
583+
error: new Error(`Failed to authenticate the SDK after reconnection: ${err}`)
584+
});
585+
586+
return false;
587+
}
588+
}
589+
590+
/**
591+
* Use the "authenticator" function to authenticate the SDK.
592+
*
593+
* @returns The authentication token
594+
*/
595+
async authenticate (): Promise<void> {
596+
if (typeof this.authenticator !== 'function') {
597+
throw new Error('The "authenticator" property must be a function.');
598+
}
599+
600+
await this.authenticator();
601+
602+
const { valid } = await this.auth.checkToken();
603+
604+
if (! valid) {
605+
throw new Error('The "authenticator" function failed to authenticate the SDK.');
606+
}
607+
}
608+
545609
/**
546610
* Adds a listener to a Kuzzle global event. When an event is fired, listeners are called in the order of their
547611
* insertion.
@@ -801,7 +865,7 @@ Discarded request: ${JSON.stringify(request)}`));
801865
uniqueQueue = {},
802866
dequeuingProcess = () => {
803867
if (this.offlineQueue.length > 0) {
804-
868+
805869
this._timeoutRequest(
806870
this.offlineQueue[0].timeout,
807871
this.offlineQueue[0].request,
@@ -853,7 +917,7 @@ Discarded request: ${JSON.stringify(request)}`));
853917

854918
/**
855919
* Sends a request with a timeout
856-
*
920+
*
857921
* @param delay Delay before the request is rejected if not resolved
858922
* @param request Request object
859923
* @param options Request options

0 commit comments

Comments
 (0)