Skip to content

Commit eb0747c

Browse files
Merge pull request lelylan#328 from lelylan/feature/refresh-persistent-access-tokens
Add support to refresh persistent access tokens
2 parents 83d4f4b + 1017e1e commit eb0747c

File tree

11 files changed

+129
-6
lines changed

11 files changed

+129
-6
lines changed

API.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ Additional options will be automatically serialized as params for the token requ
6060

6161
* `httpOptions` All [wreck](https://github.com/hapijs/wreck) options can be overriden as documented by the module `http` options.
6262

63+
#### .createToken(token) => AccessToken
64+
Creates a new access token by providing a valid plain token object.
65+
6366
### new ResourceOwnerPassword(options)
6467
This submodule provides support for the OAuth2 [Resource Owner Password Credentials](https://oauth.net/2/grant-types/password/) grant type.
6568

@@ -75,6 +78,9 @@ Additional options will be automatically serialized as params for the token requ
7578

7679
* `httpOptions` All [wreck](https://github.com/hapijs/wreck) options can be overriden as documented by the module `http` options.
7780

81+
#### .createToken(token) => AccessToken
82+
Creates a new access token by providing a valid plain token object.
83+
7884
### new ClientCredentials(options)
7985
This submodule provides support for the OAuth2 [Client Credentials](https://oauth.net/2/grant-types/client-credentials/) grant type.
8086

@@ -88,6 +94,9 @@ Additional options will be automatically serialized as params for the token requ
8894

8995
* `httpOptions` All [wreck](https://github.com/hapijs/wreck) options can be overriden as documented by the module `http` options.
9096

97+
#### .createToken(token) => AccessToken
98+
Creates a new access token by providing a valid plain token object.
99+
91100
### AccessToken
92101
#### .expired([expirationWindowSeconds]) => Boolean
93102
Determines if the current access token is definitely expired or not
@@ -107,3 +116,8 @@ Revokes either the access or refresh token depending on the {tokenType} value. T
107116

108117
#### .revokeAll() => Promise
109118
Revokes both the current access and refresh tokens
119+
120+
#### .token
121+
Immutable object containing the token object provided while constructing a new access token instance. This property will usually have the schema as specified by [RFC6750](https://tools.ietf.org/html/rfc6750#section-4), but the exact properties may vary between authorization servers.
122+
123+
Please also note, that the current implementation will always add an **expires_at** property regardless of the authorization server response, as we require it to to provide the refresh token functionality.

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Changelog
22

33
## Next
4+
### Improvements
5+
- Add support to refresh persitent access tokens
6+
47
### Maintainance
58
- Remove usage of [date-fns](https://date-fns.org/) production dependency
69
- Setup [volta](https://volta.sh/) instead of nvm to handle node versions

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,32 @@ On completion of any [supported grant type](#supported-grant-types) an access to
159159

160160
#### Refresh an access token
161161

162-
When a token expires we need a mechanism to obtain a new access token. The [AccessToken](./API.md#accesstoken) methods can be used to perform the token refresh process.
162+
On long lived applications, it is often necessary to refresh access tokens. In such scenarios the access token is usually persisted in an external database by first serializing it.
163+
164+
165+
```javascript
166+
async function run() {
167+
const accessTokenJSONString = JSON.stringify(accessToken);
168+
169+
await persistAccessTokenJSON(accessTokenJSONString);
170+
}
171+
172+
run();
173+
```
174+
175+
By the time we need to refresh the persistent access token, we can get back an [AccessToken](./API.md#accesstoken) instance by using the client's [.createToken](./API.md#createtokentoken--accesstoken) method.
176+
177+
```javascript
178+
async function run() {
179+
const accessTokenJSONString = await getPersistedAccessTokenJSON();
180+
181+
let accessToken = client.createToken(JSON.parse(accessTokenJSONString));
182+
}
183+
184+
run();
185+
```
186+
187+
Once we have determined the access token needs refreshing with the [.expired()](./API.md##expiredexpirationwindowseconds--boolean) method, we can finally refresh it with a [.refresh()](#refreshparams--promiseaccesstoken) method call.
163188

164189
```javascript
165190
async function run() {
@@ -179,7 +204,7 @@ async function run() {
179204
run();
180205
```
181206

182-
The `expired` helper is useful for knowing when a token has definitively expired. However, there is a common race condition when tokens are near expiring. If an OAuth 2.0 token is issued with a `expires_in` property (as opposed to an `expires_at` property), there can be discrepancies between the time the OAuth 2.0 server issues the access token and when it is received.
207+
The [.expired()](./API.md##expiredexpirationwindowseconds--boolean) helper is useful for knowing when a token has definitively expired. However, there is a common race condition when tokens are near expiring. If an OAuth 2.0 token is issued with a `expires_in` property (as opposed to an `expires_at` property), there can be discrepancies between the time the OAuth 2.0 server issues the access token and when it is received.
183208

184209
These come down to factors such as network and processing latency and can be worked around by preemptively refreshing the access token:
185210

lib/access-token/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,13 @@ module.exports = class AccessToken {
7979
await this.revoke(ACCESS_TOKEN_PROPERTY_NAME);
8080
await this.revoke(REFRESH_TOKEN_PROPERTY_NAME);
8181
}
82+
83+
/**
84+
* Get the access token's internal JSON representation
85+
*
86+
* @returns {String}
87+
*/
88+
toJSON() {
89+
return this.token;
90+
}
8291
};

lib/grants/authorization-code.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ module.exports = class AuthorizationCode {
4949
const parameters = GrantParams.forGrant('authorization_code', this.#config.options, params);
5050
const response = await this.#client.request(this.#config.auth.tokenPath, parameters.toObject(), httpOptions);
5151

52-
return new AccessToken(this.#config, this.#client, response);
52+
return this.createToken(response);
53+
}
54+
55+
/**
56+
* Creates a new access token instance from a plain object
57+
*
58+
* @param {Object} token Plain object representation of an access token
59+
* @returns {AccessToken}
60+
*/
61+
createToken(token) {
62+
return new AccessToken(this.#config, this.#client, token);
5363
}
5464
};

lib/grants/client-credentials.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ module.exports = class ClientCredentials {
2424
const parameters = GrantParams.forGrant('client_credentials', this.#config.options, params);
2525
const response = await this.#client.request(this.#config.auth.tokenPath, parameters.toObject(), httpOptions);
2626

27-
return new AccessToken(this.#config, this.#client, response);
27+
return this.createToken(response);
28+
}
29+
30+
/**
31+
* Creates a new access token instance from a plain object
32+
*
33+
* @param {Object} token Plain object representation of an access token
34+
* @returns {AccessToken}
35+
*/
36+
createToken(token) {
37+
return new AccessToken(this.#config, this.#client, token);
2838
}
2939
};

lib/grants/resource-owner-password.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ module.exports = class ResourceOwnerPassword {
2626
const parameters = GrantParams.forGrant('password', this.#config.options, params);
2727
const response = await this.#client.request(this.#config.auth.tokenPath, parameters.toObject(), httpOptions);
2828

29-
return new AccessToken(this.#config, this.#client, response);
29+
return this.createToken(response);
30+
}
31+
32+
/**
33+
* Creates a new access token instance from a plain object
34+
*
35+
* @param {Object} token Plain object representation of an access token
36+
* @returns {AccessToken}
37+
*/
38+
createToken(token) {
39+
return new AccessToken(this.#config, this.#client, token);
3040
}
3141
};

test/access-token.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
const test = require('ava');
44
const Chance = require('chance');
55
const accessTokenMixin = require('chance-access-token');
6-
const { isValid, isDate, differenceInSeconds } = require('date-fns');
6+
const {
7+
isValid,
8+
isDate,
9+
differenceInSeconds,
10+
isEqual,
11+
} = require('date-fns');
712

813
const AccessToken = require('../lib/access-token');
914
const Client = require('../lib/client');
@@ -125,6 +130,19 @@ test('@create => ignores the expiration parsing when no expiration property is p
125130
t.not(has(accessToken.token, 'expires_at'));
126131
});
127132

133+
test('@toJSON => serializes the access token information in an equivalent format', (t) => {
134+
const config = createModuleConfig();
135+
const client = new Client(config);
136+
137+
const accessTokenResponse = chance.accessToken();
138+
139+
const accessToken = new AccessToken(config, client, accessTokenResponse);
140+
const restoredAccessToken = new AccessToken(config, client, JSON.parse(JSON.stringify(accessToken)));
141+
142+
t.deepEqual(restoredAccessToken.token, accessToken.token);
143+
t.true(isEqual(restoredAccessToken.token.expires_at, accessToken.token.expires_at));
144+
});
145+
128146
test('@expired => returns true when expired', (t) => {
129147
const config = createModuleConfig();
130148
const client = new Client(config);

test/authorization-code.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { AuthorizationCode } = require('../index');
55
const AccessToken = require('../lib/access-token');
66
const { createModuleConfig } = require('./_module-config');
77
const {
8+
getAccessToken,
89
createAuthorizationServer,
910
getJSONEncodingScopeOptions,
1011
getFormEncodingScopeOptions,
@@ -173,6 +174,13 @@ test('@authorizeURL => returns the authorization URL with a custom module config
173174
t.is(actual, expected);
174175
});
175176

177+
test('@createToken => creates a new access token instance from a JSON object', async (t) => {
178+
const oauth2 = new AuthorizationCode(createModuleConfig());
179+
const accessToken = oauth2.createToken(getAccessToken());
180+
181+
t.true(accessToken instanceof AccessToken);
182+
});
183+
176184
test.serial('@getToken => resolves to an access token (body credentials and JSON format)', async (t) => {
177185
const expectedRequestParams = {
178186
grant_type: 'authorization_code',

test/client-credentials.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@ const { ClientCredentials } = require('../index');
55
const AccessToken = require('../lib/access-token');
66
const { createModuleConfig } = require('./_module-config');
77
const {
8+
getAccessToken,
89
createAuthorizationServer,
910
getJSONEncodingScopeOptions,
1011
getFormEncodingScopeOptions,
1112
getHeaderCredentialsScopeOptions,
1213
} = require('./_authorization-server-mock');
1314

15+
test('@createToken => creates a new access token instance from a JSON object', async (t) => {
16+
const oauth2 = new ClientCredentials(createModuleConfig());
17+
const accessToken = oauth2.createToken(getAccessToken());
18+
19+
t.true(accessToken instanceof AccessToken);
20+
});
21+
1422
test.serial('@getToken => resolves to an access token (body credentials and JSON format)', async (t) => {
1523
const expectedRequestParams = {
1624
grant_type: 'client_credentials',

0 commit comments

Comments
 (0)