Skip to content

Commit

Permalink
refactor: splits TokenService
Browse files Browse the repository at this point in the history
OKTA-468586
<<<Jenkins Check-In of Tested SHA: f1acdd4 for eng_productivity_ci_bot_okta@okta.com>>>
Artifact: okta-auth-js
Files changed count: 29
PR Link: "#1124"
  • Loading branch information
denys.oblohin authored and eng-prod-CI-bot-okta committed Mar 3, 2022
1 parent 1ef5298 commit c1146b7
Show file tree
Hide file tree
Showing 28 changed files with 1,020 additions and 334 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@

- [#1114](https://github.com/okta/okta-auth-js/pull/1114) Fixes ESM browser bundle issue by only using ESM `import` syntax

### Fixes

- [#1130](https://github.com/okta/okta-auth-js/pull/1130) `state` now stored in session during verifyEmail flow

### Other

- [#1124](https://github.com/okta/okta-auth-js/pull/1124)
- Adds multi-tab "leadership" election to prevent all tabs from renewing tokens at the same time
- Adds granular configurations for `autoRenew` (active vs passive)
- Adds options to `isAuthenticated` to override client configuration
- Fixes issue in token renew logic within `isAuthenticated`, tokens are now read from `tokenManager` (not memory) before expiration is checked

## 6.1.0

### Features
Expand Down
78 changes: 54 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ var authClient = new OktaAuth(config);

### Running as a service

By default, creating a new instance of `OktaAuth` will not create any asynchronous side-effects. However, certain features such as [token auto renew](#autorenew), [token auto remove](#autoremove) and [cross-tab synchronization](#syncstorage) require `OktaAuth` to be running as a service. This means timeouts are set in the background which will continue working until the service is stopped. To start the `OktaAuth` service, simply call the `start` method. To terminate all background processes, call `stop`.
By default, creating a new instance of `OktaAuth` will not create any asynchronous side-effects. However, certain features such as [token auto renew](#autorenew), [token auto remove](#autoremove) and [cross-tab synchronization](#syncstorage) require `OktaAuth` to be running as a service. This means timeouts are set in the background which will continue working until the service is stopped. To start the `OktaAuth` service, simply call the `start` method. To terminate all background processes, call `stop`. See [Service Configuration](#services) for more info.

```javascript
var authClient = new OktaAuth(config);
Expand Down Expand Up @@ -267,7 +267,7 @@ const authClient: OktaAuth = new OktaAuth(config)
const tokenManager: TokenManager = authClient.tokenManager;
const accessToken: AccessToken = await tokenManager.get('accessToken') as AccessToken;
const idToken: IDToken = await tokenManager.get('idToken') as IDToken;
const userInfo: UserClaims = await authClient.token.getUserInfo(accessToken, idToken);
const userInfo: UserClaims = await authClient.getUserInfo(accessToken, idToken);

if (!userInfo) {
const tokenParams: TokenParams = {
Expand Down Expand Up @@ -697,18 +697,13 @@ var config = {
```

##### `autoRenew`
> :warning: Moved to [TokenService](#tokenservice). For backwards compatibility will set `services.tokenService.autoRenew`
> :gear: Requires a [running service](#running-as-a-service)
By default, the `tokenManager` will attempt to renew tokens before they expire. If you wish to manually control token renewal, set `autoRenew` to false to disable this feature. You can listen to [`expired`](#tokenmanageronevent-callback-context) events to know when the token has expired.
##### `expireEarlySeconds`

```javascript
tokenManager: {
autoRenew: false
}
```
> :warning: DEV ONLY
Renewing tokens slightly early helps ensure a stable user experience. By default, the `expired` event will fire 30 seconds before actual expiration time. If `autoRenew` is set to true, tokens will be renewed within 30 seconds of expiration. You can customize this value by setting the `expireEarlySeconds` option. The value should be large enough to account for network latency and clock drift between the client and Okta's servers.
To facilitate a more stable user experience, tokens are considered expired 30 seconds before actual expiration time. You can customize this value by setting the `expireEarlySeconds` option. The value should be large enough to account for network latency and clock drift between the client and Okta's servers.

**NOTE** `expireEarlySeconds` option is only allowed in the **DEV** environment (localhost). It will be reset to 30 seconds when running in environments other than **DEV**.

Expand All @@ -720,23 +715,17 @@ tokenManager: {
}
```

###### `autoRemove`

> :gear: Requires a [running service](#running-as-a-service)
By default, the library will attempt to remove expired tokens during initialization when `autoRenew` is off. If you wish to to disable auto removal of tokens, set autoRemove to false.
##### `autoRemove`
> :warning: Moved to [TokenService](#tokenservice). For backwards compatibility will set `services.tokenService.autoRenew`
##### `syncStorage`
> :warning: Moved to [SyncStorageService](#syncstorageservice). For backwards compatibility will set `services.syncStorageService.enable`
> :gear: Requires a [running service](#running-as-a-service)
Automatically syncs tokens across browser tabs when token storage is `localStorage`. To disable this behavior, set `syncStorage` to false.

###### `storageKey`
##### `storageKey`

By default all tokens will be stored under the key `okta-token-storage`. You may want to change this if you have multiple apps running on a single domain which share the same storage type. Giving each app a unique storage key will prevent them from reading or writing each other's token values.

###### `storage`
##### `storage`

Specify the [storage type](#storagetype) for tokens. This will override any value set for the `token` section in the [storageManager](#storagemanager) configuration. By default, [localStorage][] will be used. This will fall back to [sessionStorage][] or [cookie][] if the previous type is not available. You may pass an object or a string. If passing an object, it should meet the requirements of a [custom storage provider](#storage). Pass a string to specify one of the built-in storage types:

Expand Down Expand Up @@ -805,6 +794,39 @@ Defaults to `none` if the `secure` option is `true`, or `lax` if the `secure` op

Defaults to `true`, set this option to false if you want to opt-out of the default clearing pendingRemove tokens behaviour when `tokenManager.start()` is called.

### `services`
> :gear: Requires a [running service](#running-as-a-service)
The following configurations require `OktaAuth` to be running as a service. See [running service](#running-as-a-service) for more info.

Default configuration:
```javascript
services: {
autoRenew: true,
autoRemove: true,
syncStorage: true,
}
```

#### `autoRenew`
When `true`, the library will attempt to renew tokens before they expire. If you wish to manually control token renewal, set `autoRenew` to `false` to disable this feature. You can listen to [`expired`](#tokenmanageronevent-callback-context) events to know when the token has expired.

> **NOTE** tokens are considered `expired` slightly before their actual expiration time. For more info, see [expireEarlySeconds](#expireearlyseconds).
In version `6.X`, the `autoRenew` configuration was set in `config.tokenManager`. To maintain backwards compatibility, this configuration is still respected but with a slight caveat. `tokenManager.autoRenew` configures 2 token auto renew strategies, `active` and `passive`.
* `active` - Network requests are made in the background in an attempt to refresh tokens before they are truly expired to maintain a seamless UX.
> :warning: this can cause an unintended side effect where the session never expires because it is constantly being refreshed (extended) before the actual expiration time
* `passive` - Token refresh attempts are only made when `oktaAuth.isAuthenticated` is called and the current tokens are determined to be expired.

When `tokenManager.autoRenew` is `true` both renew strategies are enabled. To disable the `active` strategy, set `tokenManager.autoRenew` to `true` and `services.autoRenew` to `false`. To disable both renew strategies set either `tokenManager.autoRenew` or `services.autoRenew` to `false`

#### `autoRemove`
By default, the library will attempt to remove expired tokens when `autoRenew` is `false`. If you wish to disable auto removal of tokens, set `autoRemove` to `false`.

#### `syncStorage`
Automatically syncs tokens across browser tabs when token storage is `localStorage`. To disable this behavior, set `syncStorage` to false.

This is accomplished by selecting a single tab to handle the network requests to refresh the tokens and broadcasting to the other tabs. This is done to avoid all tabs sending refresh requests simultaneously, which can cause rate limiting/throttling issues.

## API Reference
<!-- no toc -->
* [start](#start)
Expand All @@ -821,7 +843,7 @@ Defaults to `true`, set this option to false if you want to opt-out of the defau
* [verifyRecoveryToken](#verifyrecoverytokenoptions)
* [webfinger](#webfingeroptions)
* [fingerprint](#fingerprintoptions)
* [isAuthenticated](#isauthenticatedtimeout)
* [isAuthenticated](#isauthenticatedoptions)
* [getUser](#getuser)
* [getIdToken](#getidtoken)
* [getAccessToken](#getaccesstoken)
Expand Down Expand Up @@ -1052,12 +1074,20 @@ authClient.fingerprint()
})
```

### `isAuthenticated(timeout?)`
### `isAuthenticated(options?)`

> :hourglass: async
Resolves with `authState.isAuthenticated` from non-pending [authState](#authstatemanager).

`options`
* `expiredTokenBehavior`: `'renew'` (default) | `'remove'` | `'none'`
* `'renew'` - attempt to renew token before `Promise` resolves
* `'remove'` - removes token
* `'none'` - neither renews or removes expired token

> NOTE: `tokenManager.autoRenew` and `tokenManager.autoRemove` determine the default value for `expiredTokenBehavior`
### `getUser()`

> :hourglass: async
Expand Down
3 changes: 2 additions & 1 deletion jest.server.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const config = Object.assign({}, baseConfig, {
'oidc/renewToken.ts',
'oidc/renewTokens.ts',
'TokenManager/browser',
'TokenManager/crossTabs'
'SyncStorageService',
'ServiceManager'
])
});

Expand Down
28 changes: 21 additions & 7 deletions lib/OktaAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
ParseFromUrlInterface,
GetWithRedirectFunction,
RequestOptions,
IsAuthenticatedOptions,
} from './types';
import {
transactionStatus,
Expand Down Expand Up @@ -97,6 +98,7 @@ import {
clone,
} from './util';
import { TokenManager } from './TokenManager';
import { ServiceManager } from './ServiceManager';
import { get, httpRequest, setRequestHeader } from './http';
import PromiseQueue from './PromiseQueue';
import fingerprint from './browser/fingerprint';
Expand Down Expand Up @@ -154,6 +156,7 @@ class OktaAuth implements OktaAuthInterface, SigninAPI, SignoutAPI {
emitter: any;
tokenManager: TokenManager;
authStateManager: AuthStateManager;
serviceManager: ServiceManager;
http: HttpAPI;
fingerprint: FingerprintAPI;
_oktaUserAgent: OktaUserAgent;
Expand Down Expand Up @@ -350,17 +353,24 @@ class OktaAuth implements OktaAuthInterface, SigninAPI, SignoutAPI {

// AuthStateManager
this.authStateManager = new AuthStateManager(this);

// ServiceManager
this.serviceManager = new ServiceManager(this, args.services);
}

start() {
// TODO: review tokenManager.start
this.tokenManager.start();
if (!this.token.isLoginRedirect()) {
this.authStateManager.updateAuthState();
}
this.serviceManager.start();
}

stop() {
// TODO: review tokenManager.stop
this.tokenManager.stop();
this.serviceManager.stop();
}

setHeaders(headers) {
Expand Down Expand Up @@ -567,33 +577,37 @@ class OktaAuth implements OktaAuthInterface, SigninAPI, SignoutAPI {

// Returns true if both accessToken and idToken are not expired
// If `autoRenew` option is set, will attempt to renew expired tokens before returning.
async isAuthenticated(): Promise<boolean> {

let { accessToken, idToken } = this.tokenManager.getTokensSync();
async isAuthenticated(options: IsAuthenticatedOptions = {}): Promise<boolean> {
// TODO: remove dependency on tokenManager options in next major version - OKTA-473815
const { autoRenew, autoRemove } = this.tokenManager.getOptions();

const shouldRenew = options.onExpiredToken ? options.onExpiredToken === 'renew' : autoRenew;
const shouldRemove = options.onExpiredToken ? options.onExpiredToken === 'remove' : autoRemove;

let { accessToken } = this.tokenManager.getTokensSync();
if (accessToken && this.tokenManager.hasExpired(accessToken)) {
accessToken = undefined;
if (autoRenew) {
if (shouldRenew) {
try {
accessToken = await this.tokenManager.renew('accessToken') as AccessToken;
} catch {
// Renew errors will emit an "error" event
}
} else if (autoRemove) {
} else if (shouldRemove) {
this.tokenManager.remove('accessToken');
}
}

let { idToken } = this.tokenManager.getTokensSync();
if (idToken && this.tokenManager.hasExpired(idToken)) {
idToken = undefined;
if (autoRenew) {
if (shouldRenew) {
try {
idToken = await this.tokenManager.renew('idToken') as IDToken;
} catch {
// Renew errors will emit an "error" event
}
} else if (autoRemove) {
} else if (shouldRemove) {
this.tokenManager.remove('idToken');
}
}
Expand Down

0 comments on commit c1146b7

Please sign in to comment.