Skip to content

Commit

Permalink
Merge pull request #52 from Authress/improve-timeout-documentation
Browse files Browse the repository at this point in the history
Improve documentation on inputOptions.timeoutInMillis. #51.
  • Loading branch information
wparad authored Sep 21, 2024
2 parents 00b331d + 212b71a commit 29a1f02
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 3 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"embeddable",
"esbuild",
"filemanager",
"multifactor",
"totp"
]
}
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This is the changelog for [Authress Login](readme.md).
* [Fix] Force a sessionCheck after a logout.
* Validate logout redirect urls to ensure they are valid before attempting to log the user out.
* [Fix] enable iOS 'Load Failed' non-compliant HTTP Fetch retries.
* [Fix] Improve support for timeoutInMillis for long values.

## 2.4 ##
* Prevent silent returns from `authenticate` when a different connectionId is used to have the user log in.
Expand Down
48 changes: 48 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,53 @@
# Troubleshooting usage

## Why do I get a timeout when calling `ensureToken`

`ensureToken` attempts to wait for a token to be available. It attempts to block code execution until one is. The reason for this is that `useSessionExists` and `authenticate` might be called in a separate thread or a separate browser tab. When this happens multiple user actions or site code flows might result in many API requests to your backend all depending on `ensureToken`. Without the block, all these requests would be sent without a token (worst case) or throw an error everywhere in your code based (better case).

When a user session exists but the token is expired, instead we use the session to generate a new token. To prevent the race condition and causing the API requests that depend on a valid token from throwing error, `ensureToken` blocks (best case)

But what if there is no session, and the user must log in?

In the past `ensureToken` blocked forever, however this can create an unexpected situation in your site where the code "gets stuck", because methods that do that are often unexpected in libraries. So instead we enable a timeout for when you feel how long it is reasonable to wait for a token. The default timeout is set to be 5 seconds.

`ensureToken` seeks to never return `null`, as that can create a `pit of failure`, instead it throws.

The recommended flow for interacting with the login SDK could be instead something like this in one place in your whole site:
```js
const loggedIn = await client.userSessionExists();
if (!loggedIn) {
await client.authenticate();
return;
}

// And then everywhere else you need a token:
(Example Code Location: Site 1)
const token = await client.ensureToken();
```

Additionally worth mentioning is that `ensureToken` actually already calls userSessionExists. So code like this is never necessary:
```js
const loggedIn = await client.userSessionExists()
if (loggedIn) {
const token = await client.ensureToken({})
// Use token
}
```

And instead can be simplified to be:

(Example Code Location: Site 1)
```js
try {
const token = await client.ensureToken();
// Use token
} catch (error) {
// Do something else
}
```

Additionally, it is important to note that this @authress/login library already handles all the necessary token state management. If you have places in your code based that include `setSessionToken` and somewhere else with `getSessionToken` (For example in a Site 2 location, called site 1 and site 2 to indicate these are in the code based in very different locations), instead, delete all the "site 1" code and then at "site 2" where you are presumably where `getSessionToken` was being called, directly call into the library with `const token = await client.ensureToken()`.

## Buffer is not defined (esbuild/vite)
There are some modules used by Authress Login which require polyfills for the browser. Some bundlers such as Rollup and Webpack pull these in automatically, under certain cases. Others do not. If you run into an issue, make sure to check the documentation.

Expand Down
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export class LoginClient {

/**
* @description Ensures the user's bearer token exists. To be used in the Authorization header as a Bearer token. This method blocks on a valid user session being created, and expects {@link authenticate} to have been called first. Additionally, if the application configuration specifies that tokens should be secured from javascript, the token will be a hidden cookie only visible to service APIs and will not be returned. If the token is expired and the session is still valid, then it will automatically generate a new token directly from Authress.
* @param {TokenParameters} [settings] Optional token parameters to constrain how the existing token is retreived.
* @param {TokenParameters} [settings] Optional token parameters to constrain how the existing token is retrieved.
* @return {Promise<string>} The Authorization Bearer token.
*/
ensureToken(settings?: TokenParameters): Promise<string>;
Expand Down
13 changes: 11 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -670,13 +670,22 @@ class LoginClient {
* @description Ensures the user's bearer token exists. To be used in the Authorization header as a Bearer token. This method blocks on a valid user session being created, and expects {@link authenticate} to have been called first. Additionally, if the application configuration specifies that tokens should be secured from javascript, the token will be a hidden cookie only visible to service APIs and will not be returned. If the token is expired and the session is still valid, then it will automatically generate a new token directly from Authress.
* @param {Object} [options] Options for getting a token including timeout configuration.
* @param {Number} [options.timeoutInMillis=5000] Timeout waiting for user token to populate. After this time an error will be thrown.
* @return {Promise<String>} The Authorization Bearer token if allowed otherwise null.
* @return {Promise<String>} The Authorization Bearer token
* @throws {TokenTimeout} After the timeout if no session was found. By default waits for 5000 for another thread to continue the session, after which if still no token exists, will throw
*/
async ensureToken(options) {
// Using this function blocks all ensureToken calls on a single session continuation, this is required.
await this.userSessionExists();

// Timeout after the timeout so that all threads don't "get stuck" in an unexpected way for consumers of the library.
const inputOptions = Object.assign({ timeoutInMillis: 5000 }, options || {});
const sessionWaiterAsync = this.waitForUserSession();
const timeoutAsync = new Promise((resolve, reject) => setTimeout(reject, inputOptions.timeoutInMillis || 0));

// The max timeout is -1 or Infinity, however `setTimeout` does really like that, often the max allowed value is 2**31 - 1 (MAX_INT), so we'll use that instead
// https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value
const timeoutInMillis = inputOptions.timeoutInMillis === -1 || inputOptions.timeoutInMillis > (2 ** 31 - 1) ? (2 ** 31 - 1) : inputOptions.timeoutInMillis;

const timeoutAsync = new Promise((resolve, reject) => setTimeout(reject, timeoutInMillis || 0));
try {
await Promise.race([sessionWaiterAsync, timeoutAsync]);
} catch (timeout) {
Expand Down

0 comments on commit 29a1f02

Please sign in to comment.