Skip to content

Commit

Permalink
Add "onSessionExpired" option (#271)
Browse files Browse the repository at this point in the history
* Add "onSessionExpired" option

* release: 2.11.2
  • Loading branch information
aarongranick-okta authored Jan 9, 2020
1 parent 37ec1bc commit 6774a8e
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 44 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 2.11.2

### Features

- [#271](https://github.com/okta/okta-auth-js/pull/271) - New option `onSessionExpired`

## 2.11.1

### Other
Expand Down
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ tokenManager: {
| `ignoreSignature` | ID token signatures are validated by default when `token.getWithoutPrompt`, `token.getWithPopup`, `token.getWithRedirect`, and `token.verify` are called. To disable ID token signature validation for these methods, set this value to `true`. |
| | This option should be used only for browser support and testing purposes. |
| `maxClockSkew` | Defaults to 300 (five minutes). This is the maximum difference allowed between a client's clock and Okta's, in seconds, when validating tokens. Setting this to 0 is not recommended, because it increases the likelihood that valid tokens will fail validation.
| `onSessionExpired` | A function to be called when the Okta SSO session has expired or was ended outside of the application. A typical handler would initiate a login flow.
| `tokenManager` | *(optional)*: An object containing additional properties used to configure the internal token manager. |

* `autoRenew`:
Expand Down Expand Up @@ -237,6 +238,10 @@ var config = {
// TokenManager config
tokenManager: {
storage: 'sessionStorage'
},

onSessionExpired: function() {
console.log('re-authorization is required');
}
};

Expand Down Expand Up @@ -1513,7 +1518,6 @@ The following configuration options can **only** be included in `token.getWithou

For a list of all available parameters that can be passed to the `/authorize` endpoint, see Okta's [Authorize Request API](https://developer.okta.com/docs/api/resources/oidc#request-parameters).


##### Example

```javascript
Expand Down Expand Up @@ -1584,13 +1588,12 @@ authClient.token.getWithRedirect(oauthOptions);

#### `token.parseFromUrl(options)`

Parses the authorization code, access, or ID Tokens from the URL after a successful authentication redirect.
Parses the authorization code, access, or ID Tokens from the URL after a successful authentication redirect.

If an authorization code is present, it will be exchanged for token(s) by posting to the `tokenUrl` endpoint.
If an authorization code is present, it will be exchanged for token(s) by posting to the `tokenUrl` endpoint.

The ID token will be [verified and validated](https://github.com/okta/okta-auth-js/blob/master/lib/token.js#L186-L190) before available for use.


```javascript
authClient.token.parseFromUrl()
.then(function(tokenOrTokens) {
Expand Down Expand Up @@ -1732,7 +1735,7 @@ authClient.tokenManager.clear();

#### `tokenManager.renew(key)`

Manually renew a token before it expires.
Manually renew a token before it expires and update the stored value.

* `key` - Key for the token you want to renew

Expand Down Expand Up @@ -1760,28 +1763,30 @@ Subscribe to an event published by the `tokenManager`.
* `context` - Optional context to bind the callback to

```javascript
// Triggered when the token has expired
// Triggered when a token has expired
authClient.tokenManager.on('expired', function (key, expiredToken) {
console.log('Token with key', key, ' has expired:');
console.log(expiredToken);
});

// Triggered when a token has been renewed
authClient.tokenManager.on('renewed', function (key, newToken, oldToken) {
console.log('Token with key', key, 'has been renewed');
console.log('Old token:', oldToken);
console.log('New token:', newToken);
});

// Triggered when an OAuthError is returned via the API
// Triggered when an OAuthError is returned via the API (typically during auto-renew)
authClient.tokenManager.on('error', function (err) {
console.log('TokenManager error:', err);
// err.name
// err.message
// err.errorCode
// err.errorSummary

if (err.errorCode === 'login_required') {
// Return to unauthenticated state
// err.tokenKey
// err.accessToken
if (err.errorCode === 'login_required' && err.accessToken) {
// The Okta session has expired or was closed outside the application
// The application should return to an unauthenticated state
// This error can also be handled using the 'onSessionExpired' option
}
});
```
Expand All @@ -1790,10 +1795,10 @@ authClient.tokenManager.on('error', function (err) {

Unsubscribe from `tokenManager` events. If no callback is provided, unsubscribes all listeners from the event.

* `event` - Event to unsubscribe from
* `event` - Event to unsubscribe from
* `callback` - Optional callback that was used to subscribe to the event

```javascript
```javascript
authClient.tokenManager.off('renewed');
authClient.tokenManager.off('renewed', myRenewedCallback);
```
Expand Down Expand Up @@ -1844,7 +1849,7 @@ Since the Node library can be used only for the Authentication flow, it implemen

The main difference is that the Node library does **not** have a `session.setCookieAndRedirect` function, so you will have to redirect by yourself (for example using `res.redirect('https://www.yoursuccesspage.com')`).

The `SUCCESS` transaction will still include a `sessionToken` which you can use with the session APIs: https://github.com/okta/okta-sdk-nodejs#sessions.
The `SUCCESS` transaction will still include a `sessionToken` which you can use with the session APIs: <https://github.com/okta/okta-sdk-nodejs#sessions.>

## Building the SDK

Expand Down
5 changes: 3 additions & 2 deletions packages/okta-auth-js/lib/TokenManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ var util = require('./util');
var AuthSdkError = require('./errors/AuthSdkError');
var storageUtil = require('./browser/browserStorage');
var Q = require('q');
var Emitter = require('tiny-emitter');
var constants = require('./constants');
var storageBuilder = require('./storageBuilder');
var SdkClock = require('./clock');
Expand Down Expand Up @@ -184,6 +183,8 @@ function renew(sdk, tokenMgmtRef, storage, key) {
.catch(function(err) {
if (err.name === 'OAuthError' || err.name === 'AuthSdkError') {
remove(tokenMgmtRef, storage, key);
err.tokenKey = key;
err.accessToken = !!token.accessToken;
emitError(tokenMgmtRef, err);
}
throw err;
Expand Down Expand Up @@ -242,7 +243,7 @@ function TokenManager(sdk, options) {
var tokenMgmtRef = {
clock: clock,
options: options,
emitter: new Emitter(),
emitter: sdk.emitter,
expireTimeouts: {},
renewPromise: {}
};
Expand Down
17 changes: 16 additions & 1 deletion packages/okta-auth-js/lib/browser/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

require('../vendor/polyfills');

var Emitter = require('tiny-emitter');
var AuthSdkError = require('../errors/AuthSdkError');
var builderUtil = require('../builderUtil');
var constants = require('../constants');
Expand Down Expand Up @@ -50,7 +51,8 @@ function OktaAuthBuilder(args) {
httpRequestClient: args.httpRequestClient,
storageUtil: args.storageUtil,
transformErrorXHR: args.transformErrorXHR,
headers: args.headers
headers: args.headers,
onSessionExpired: args.onSessionExpired,
};

if (this.options.pkce && !sdk.features.isPKCESupported()) {
Expand Down Expand Up @@ -154,10 +156,23 @@ function OktaAuthBuilder(args) {
return agent && !isWindowsPhone.test(agent);
};

sdk.emitter = new Emitter();
sdk.tokenManager = new TokenManager(sdk, args.tokenManager);
sdk.tokenManager.on('error', this._onTokenManagerError, this);
}

var proto = OktaAuthBuilder.prototype;
proto._onTokenManagerError = function(error) {
var code = error.errorCode;
if (code === 'login_required' && error.accessToken) {
if (this.options.onSessionExpired) {
this.options.onSessionExpired();
} else {
// eslint-disable-next-line no-console
console.error('Session has expired or was closed outside the application.');
}
}
};

proto.features = {};

Expand Down
2 changes: 1 addition & 1 deletion packages/okta-auth-js/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@okta/okta-auth-js",
"description": "The Okta Auth SDK",
"version": "2.11.1",
"version": "2.11.2",
"homepage": "https://github.com/okta/okta-auth-js",
"license": "Apache-2.0",
"main": "lib/server/serverIndex.js",
Expand Down
49 changes: 49 additions & 0 deletions packages/okta-auth-js/test/spec/browser.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
jest.mock('cross-fetch');

var Emitter = require('tiny-emitter');
var OktaAuth = require('../../lib/browser/browserIndex');
var AuthApiError = require('../../lib/errors/AuthApiError');

Expand All @@ -11,6 +14,52 @@ describe('Browser', function() {
expect(auth instanceof OktaAuth).toBe(true);
});

describe('Error handling', function() {
it('Listens to error events from TokenManager', function() {
jest.spyOn(Emitter.prototype, 'on');
jest.spyOn(OktaAuth.prototype, '_onTokenManagerError');
var auth = new OktaAuth({ url: 'http://localhost/fake' });
expect(Emitter.prototype.on).toHaveBeenCalledWith('error', auth._onTokenManagerError, auth);
var emitter = Emitter.prototype.on.mock.instances[0];
var error = { errorCode: 'anything'};
emitter.emit('error', error);
expect(OktaAuth.prototype._onTokenManagerError).toHaveBeenCalledWith(error);
});

it('error with errorCode "login_required" and accessToken: true will call option "onSessionExpired" function', function() {
var onSessionExpired = jest.fn();
jest.spyOn(Emitter.prototype, 'on');
new OktaAuth({ url: 'http://localhost/fake', onSessionExpired: onSessionExpired });
var emitter = Emitter.prototype.on.mock.instances[0];
expect(onSessionExpired).not.toHaveBeenCalled();
var error = { errorCode: 'login_required', accessToken: true };
emitter.emit('error', error);
expect(onSessionExpired).toHaveBeenCalled();
});

it('error with errorCode "login_required" (not accessToken) does not call option "onSessionExpired" function', function() {
var onSessionExpired = jest.fn();
jest.spyOn(Emitter.prototype, 'on');
new OktaAuth({ url: 'http://localhost/fake', onSessionExpired: onSessionExpired });
var emitter = Emitter.prototype.on.mock.instances[0];
expect(onSessionExpired).not.toHaveBeenCalled();
var error = { errorCode: 'login_required' };
emitter.emit('error', error);
expect(onSessionExpired).not.toHaveBeenCalled();
});

it('error with unknown errorCode does not call option "onSessionExpired" function', function() {
var onSessionExpired = jest.fn();
jest.spyOn(Emitter.prototype, 'on');
new OktaAuth({ url: 'http://localhost/fake', onSessionExpired: onSessionExpired });
var emitter = Emitter.prototype.on.mock.instances[0];
expect(onSessionExpired).not.toHaveBeenCalled();
var error = { errorCode: 'unknown', accessToken: true };
emitter.emit('error', error);
expect(onSessionExpired).not.toHaveBeenCalled();
});
});

describe('options', function() {

describe('PKCE', function() {
Expand Down
Loading

0 comments on commit 6774a8e

Please sign in to comment.