Skip to content

Conversation

@Aschen
Copy link
Contributor

@Aschen Aschen commented Feb 15, 2021

What does this PR do?

When resubscribing after a reconnected event, the SDK use the original subscription request to re-subscribe.

The Kuzzle.query method mutate request and inject the JWT inside. Meaning that the request saved for the re-subscription already has a JWT inside.

This can be an issue because developers rely on the kuzzle.jwt property of the SDK to know if 1) the token is still valid 2) there are connection information.

Now the request and options parameter of the query method are cloned before modification. (:warning: to save resources it's not a deep clone so only the top level properties are duplicated. Nested objects will not be cloned but only referenced)

How should this be manually tested?

Run a Kuzzle stack and then create this user and update anonymous role:

kourou security:createUser '{                                                             
  content: {
    profileIds: ["default"]
  },
  credentials: {
    local: {
      username: "test-admin",
      password: "password"
    },
  }
}'

kourou security:updateRole '{                                                             
  controllers: {
    "realtime": {
      actions: {
        "subscribe": false
      }
    },
    "*": {
      actions: {
        "*": true
      }
    }
  }
}'

Then run this script

resubscribe.js
const { Kuzzle, WebSocket } = require('./index');

const kuzzle = new Kuzzle(new WebSocket('localhost'));

async function subscribeToNotifications () {
  console.log('subscribeToNotifications');
  await kuzzle.realtime.subscribe('test', 'test', {}, () => {
    console.log('Notification received');
  });
  console.log('Subscribed');
}

// authenticate and subscribe after a "tokenExpired" event
kuzzle.on('tokenExpired', async () => {
  await authenticateAndSubscribe();
});

kuzzle.on('reconnected', async () => {
  await authenticateAndSubscribe();
})

// authenticate and then subscribe
// re-try every 1 second until it works
async function authenticateAndSubscribe () {
  console.log('authenticateAndSubscribe()');
  try {
    const { valid } = await kuzzle.auth.checkToken();

    if (! valid) {
      await kuzzle.auth.login('local', { username: 'test-admin', password: 'password' }, '5s');
      console.log('Authenticated');
    }
    else {
      console.log('Already authenticated');
    }

    await subscribeToNotifications();
  }
  catch (error) {
    console.log('authenticateAndSubscribe ', error);
    // Wrong or revoked credentials
    if (error.id === 'plugin.strategy.missing_user') {
      throw error;
    }

    // An error occured (network, etc), try again in 1 sec
    setTimeout(async () => {
      try {
        await authenticateAndSubscribe();
      }
      catch (error) {
        // if we end up there it means that we don't have correct credentials anymore
      }
    }, 1000);
  }
}

async function refreshToken () {
  setInterval(async () => {
    try {
      await kuzzle.auth.refreshToken({ expiresIn: '5s' });
      console.log('Refreshed')
    }
    catch (error) {
      console.log('Cannot refresh token: ', error);
    }
  }, 4000);
}

async function run () {
  await kuzzle.connect();
  await authenticateAndSubscribe();

  await refreshToken();
}

run();
Logs
➜  sdk-javascript git:(fix-resubscription) ✗ node -r ts-node/register test.js
authenticateAndSubscribe()
Authenticated
subscribeToNotifications
Subscribed
Cannot refresh token:  Error: Unable to execute request: not connected to a Kuzzle server.
Discarded request: {"action":"refreshToken","expiresIn":"5s","controller":"auth","requestId":"02248d26-4cd9-4661-b5bf-628c95b30063","volatile":{"sdkInstanceId":"f346463c-2674-4753-8ab6-4ad8054157cd","sdkName":"js@7.5.3"},"jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI4MjcwZTIxMS1kM2E5LTQzM2UtYTgzMS02ODkzZDUwMzU3ZDgiLCJpYXQiOjE2MTMzOTU1NTMsImV4cCI6MTYxMzM5NTU1OH0.cSfYw97kY_uah623nCRUnpOjXw6Zp1eJtEb3lj7zfwk"}
    at WebSocketProtocol.query (/home/aschen/projets/kuzzleio/sdk-javascript/src/protocols/abstract/Base.ts:121:29)
    at Kuzzle.query (/home/aschen/projets/kuzzleio/sdk-javascript/src/Kuzzle.ts:589:26)
    at AuthController.query (/home/aschen/projets/kuzzleio/sdk-javascript/src/controllers/Base.ts:41:25)
    at AuthController.refreshToken (/home/aschen/projets/kuzzleio/sdk-javascript/src/controllers/Auth.ts:534:17)
    at Timeout._onTimeout (/home/aschen/projets/kuzzleio/sdk-javascript/test.js:57:25)
    at listOnTimeout (internal/timers.js:549:17)
    at processTimers (internal/timers.js:492:7)
Cannot refresh token:  Error: Unable to execute request: not connected to a Kuzzle server.
Discarded request: {"action":"refreshToken","expiresIn":"5s","controller":"auth","requestId":"ac2f20c0-5bbf-4469-8f91-e916db0d10e9","volatile":{"sdkInstanceId":"f346463c-2674-4753-8ab6-4ad8054157cd","sdkName":"js@7.5.3"},"jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI4MjcwZTIxMS1kM2E5LTQzM2UtYTgzMS02ODkzZDUwMzU3ZDgiLCJpYXQiOjE2MTMzOTU1NTMsImV4cCI6MTYxMzM5NTU1OH0.cSfYw97kY_uah623nCRUnpOjXw6Zp1eJtEb3lj7zfwk"}
    at WebSocketProtocol.query (/home/aschen/projets/kuzzleio/sdk-javascript/src/protocols/abstract/Base.ts:121:29)
    at Kuzzle.query (/home/aschen/projets/kuzzleio/sdk-javascript/src/Kuzzle.ts:589:26)
    at AuthController.query (/home/aschen/projets/kuzzleio/sdk-javascript/src/controllers/Base.ts:41:25)
    at AuthController.refreshToken (/home/aschen/projets/kuzzleio/sdk-javascript/src/controllers/Auth.ts:534:17)
    at Timeout._onTimeout (/home/aschen/projets/kuzzleio/sdk-javascript/test.js:57:25)
    at listOnTimeout (internal/timers.js:549:17)
    at processTimers (internal/timers.js:492:7)
authenticateAndSubscribe()
Authenticated
subscribeToNotifications
Subscribed
Refreshed
Refreshed

Wait 2 sec and then stop Kuzzle:

  • refreshToken request should be queued
  • networkError should appear

Restart Kuzzle:

  • the automatic re-subscription will fail because the token is expired
  • the script should re-authenticate
  • only 1 subscription is registered (try kourou realtime:publish test test '{ toto: 42 }' --username test-admin --password password )

Other changes

  • remove the checkToken when the SDK reconnect because it breaks the workflow on tokenExpired events

Aschen added 2 commits February 15, 2021 14:30
@codecov
Copy link

codecov bot commented Feb 15, 2021

Codecov Report

Merging #602 (09f73b6) into 7-dev (939cce7) will increase coverage by 0.04%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##            7-dev     #602      +/-   ##
==========================================
+ Coverage   87.35%   87.39%   +0.04%     
==========================================
  Files          33       33              
  Lines        1479     1476       -3     
  Branches      260      258       -2     
==========================================
- Hits         1292     1290       -2     
+ Misses        136      135       -1     
  Partials       51       51              
Impacted Files Coverage Δ
src/Kuzzle.ts 86.88% <100.00%> (+0.24%) ⬆️
src/protocols/WebSocket.ts 90.76% <100.00%> (ø)
src/protocols/abstract/Base.ts 92.30% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 939cce7...09f73b6. Read the comment docs.

Copy link
Contributor

@scottinet scottinet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple nitpickings

src/Kuzzle.ts Outdated
Comment on lines 526 to 527
const request = { ...req };
const options = { ...opts };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nitpicking) or you could make a deep-copy, as it is safer in case users modify the referenced objects before they are submitted again, if they get queued

Suggested change
const request = { ...req };
const options = { ...opts };
const request = JSON.parse(JSON.stringify(req));
const options = JSON.parse(JSON.stringify(opts));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I was doing at first but then I thought about the performance drop and I sticked to the light copy.
The query method is only mutating first level properties so IMHO it's enough and if users mutate there own payloads I think it's not our responsibility anymore

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About performances, I believe the JSON.stringify/JSON.parse method performs at roughly a couple millions instructions per second, so we should be quite safe there.

Aschen and others added 3 commits February 16, 2021 18:36
Co-authored-by: Sébastien Cottinet <scottinet@protonmail.com>
@Aschen Aschen merged commit 22ec82c into 7-dev Feb 17, 2021
@Aschen Aschen deleted the fix-resubscription branch February 17, 2021 06:11
@Aschen Aschen mentioned this pull request Feb 17, 2021
Aschen added a commit that referenced this pull request Feb 17, 2021
# [7.5.4](https://github.com/kuzzleio/sdk-javascript/releases/tag/7.5.4) (2021-02-17)


#### Bug fixes

- [ [#602](#602) ] Fix re-subscription on re-connection ([Aschen](https://github.com/Aschen))

#### Enhancements

- [ [#586](#586) ] Add keep-alive mechanism to detect connection lost with realtime protocols   ([Yoann-Abbes](https://github.com/Yoann-Abbes))
---
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants