Skip to content

Commit

Permalink
feat: dynamic token expiration
Browse files Browse the repository at this point in the history
It is now possible to provide a `function` instead of a `Number` to
return the number of seconds a given token should be active for.

This function will be triggered for every instantiated token with the
token as a first and client instance as a second argument. These give
enough inside on the token's attributes as well as allow for client
metadata such as applicationType to be checked in order to return the
expected TTL.

BREAKING CHANGE: In order for dynamic token expiration to be able to
pass a client instance to the helpers it is now better to pass a
`client` property being the client instance to a new token instance
rather then a `clientId`. When passing a client the `clientId` will be
set automatically.
  • Loading branch information
panva committed Sep 26, 2018
1 parent d334574 commit 6788b83
Show file tree
Hide file tree
Showing 17 changed files with 359 additions and 36 deletions.
28 changes: 24 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,16 +450,15 @@ provider.registerGrantType('password', function passwordGrantTypeFactory(provide
const at = new AccessToken({
gty: 'password',
accountId: account.id,
clientId: ctx.oidc.client.clientId,
client: ctx.oidc.client,
grantId: ctx.oidc.uuid,
});

const accessToken = await at.save();
const expiresIn = AccessToken.expiresIn;

ctx.body = {
access_token: accessToken,
expires_in: expiresIn,
expires_in: at.expiration,
token_type: 'Bearer',
};
} else {
Expand Down Expand Up @@ -1862,7 +1861,7 @@ _**default value**_:

### ttl

Expirations (in seconds) for all token types
Expirations (in seconds, or dynamically returned value) for all token types

_**affects**_: tokens
<details>
Expand All @@ -1880,6 +1879,27 @@ _**affects**_: tokens

</details>

<details>
<summary>(Click to expand) To resolve a ttl on runtime for each new token</summary>
<br>


Configure `ttl` for a given token type with a function like so, this must return a value, not a Promise.


```js
{
ttl: {
AccessToken(token, client) {
// return a Number (in seconds) for the given token (first argument), the associated client is
// passed as a second argument
// Tip: if the values are entirely client based memoize the results
return resolveTTLfor(token, client);
},
},
}
```
</details>

### uniqueness

Expand Down
2 changes: 1 addition & 1 deletion lib/actions/authorization/device_authorization_response.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ module.exports = function getDeviceAuthorizationResponse(provider) {
const userCode = generate(charset, mask);

const dc = new DeviceCode({
client: ctx.oidc.client,
grantId: ctx.oidc.uuid,
clientId: ctx.oidc.client.clientId,
params: ctx.oidc.params.toPlainObject(),
userCode: normalize(userCode),
deviceInfo: deviceInfo(ctx),
Expand Down
4 changes: 2 additions & 2 deletions lib/actions/authorization/process_response_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ module.exports = (provider) => {
async function tokenHandler(ctx) {
const accountId = ctx.oidc.session.accountId();
const at = new AccessToken({
client: ctx.oidc.client,
accountId,
claims: ctx.oidc.resolvedClaims(),
clientId: ctx.oidc.client.clientId,
grantId: ctx.oidc.uuid,
scope: ctx.oidc.acceptedScope(),
sid: ctx.oidc.session.sidFor(ctx.oidc.client.clientId),
Expand All @@ -40,7 +40,7 @@ module.exports = (provider) => {
amr: ctx.oidc.amr,
authTime: ctx.oidc.session.authTime(),
claims: ctx.oidc.resolvedClaims(),
clientId: ctx.oidc.client.clientId,
client: ctx.oidc.client,
grantId: ctx.oidc.uuid,
nonce: ctx.oidc.params.nonce,
redirectUri: ctx.oidc.params.redirect_uri,
Expand Down
8 changes: 3 additions & 5 deletions lib/actions/grants/authorization_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ module.exports.handler = function getAuthorizationCodeHandler(provider) {

const { AccessToken, IdToken, RefreshToken } = provider;
const at = new AccessToken({
client: ctx.oidc.client,
gty,
accountId: account.accountId,
claims: code.claims,
clientId: ctx.oidc.client.clientId,
grantId: code.grantId,
scope: code.scope,
sid: code.sid,
Expand All @@ -77,8 +77,6 @@ module.exports.handler = function getAuthorizationCodeHandler(provider) {
const accessToken = await at.save();
ctx.oidc.entity('AccessToken', at);

const { expiresIn } = AccessToken;

let refreshToken;
const grantPresent = ctx.oidc.client.grantTypes.includes('refresh_token');

Expand All @@ -90,7 +88,7 @@ module.exports.handler = function getAuthorizationCodeHandler(provider) {
amr: code.amr,
authTime: code.authTime,
claims: code.claims,
clientId: ctx.oidc.client.clientId,
client: ctx.oidc.client,
grantId: code.grantId,
nonce: code.nonce,
scope: code.scope,
Expand Down Expand Up @@ -129,7 +127,7 @@ module.exports.handler = function getAuthorizationCodeHandler(provider) {

ctx.body = {
access_token: accessToken,
expires_in: expiresIn,
expires_in: at.expiration,
id_token: idToken,
refresh_token: refreshToken,
scope: code.scope,
Expand Down
2 changes: 1 addition & 1 deletion lib/actions/grants/client_credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ module.exports.handler = function getClientCredentialsHandler(provider) {
}) : [];

const token = new ClientCredentials({
clientId: ctx.oidc.client.clientId,
client: ctx.oidc.client,
scope: scopes.join(' ') || undefined,
});

Expand Down
8 changes: 3 additions & 5 deletions lib/actions/grants/device_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ module.exports.handler = function getDeviceCodeHandler(provider) {
gty,
accountId: account.accountId,
claims: code.claims,
clientId: ctx.oidc.client.clientId,
client: ctx.oidc.client,
grantId: code.grantId,
scope: code.scope,
sid: code.sid,
Expand All @@ -93,20 +93,18 @@ module.exports.handler = function getDeviceCodeHandler(provider) {
const accessToken = await at.save();
ctx.oidc.entity('AccessToken', at);

const { expiresIn } = AccessToken;

let refreshToken;
const grantPresent = ctx.oidc.client.grantTypes.includes('refresh_token');

if (grantPresent && (alwaysIssueRefresh || code.scope.split(' ').includes('offline_access'))) {
const rt = new RefreshToken({
client: ctx.oidc.client,
gty,
accountId: account.accountId,
acr: code.acr,
amr: code.amr,
authTime: code.authTime,
claims: code.claims,
clientId: ctx.oidc.client.clientId,
grantId: code.grantId,
nonce: code.nonce,
scope: code.scope,
Expand Down Expand Up @@ -147,7 +145,7 @@ module.exports.handler = function getDeviceCodeHandler(provider) {

ctx.body = {
access_token: accessToken,
expires_in: expiresIn,
expires_in: at.expiration,
id_token: idToken,
refresh_token: refreshToken,
scope: code.scope,
Expand Down
7 changes: 3 additions & 4 deletions lib/actions/grants/refresh_token.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ module.exports.handler = function getRefreshTokenHandler(provider) {
ctx.oidc.entity('RotatedRefreshToken', refreshToken);

refreshToken = new RefreshToken({
client: ctx.oidc.client,
scope: refreshToken.scope,
accountId: refreshToken.accountId,
acr: refreshToken.acr,
amr: refreshToken.amr,
authTime: refreshToken.authTime,
claims: refreshToken.claims,
clientId: refreshToken.clientId,
grantId: refreshToken.grantId,
nonce: refreshToken.nonce,
sid: refreshToken.sid,
Expand All @@ -97,10 +97,10 @@ module.exports.handler = function getRefreshTokenHandler(provider) {
}

const at = new AccessToken({
client: ctx.oidc.client,
scope,
accountId: account.accountId,
claims: refreshToken.claims,
clientId: ctx.oidc.client.clientId,
grantId: refreshToken.grantId,
sid: refreshToken.sid,
gty: refreshToken.gty,
Expand All @@ -114,7 +114,6 @@ module.exports.handler = function getRefreshTokenHandler(provider) {

const accessToken = await at.save();
ctx.oidc.entity('AccessToken', at);
const { expiresIn } = AccessToken;

const claims = _.get(refreshToken, 'claims.id_token', {});
const rejected = _.get(refreshToken, 'claims.rejected', []);
Expand Down Expand Up @@ -142,7 +141,7 @@ module.exports.handler = function getRefreshTokenHandler(provider) {

ctx.body = {
access_token: accessToken,
expires_in: expiresIn,
expires_in: at.expiration,
id_token: idToken,
refresh_token: refreshTokenValue,
scope,
Expand Down
2 changes: 1 addition & 1 deletion lib/actions/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ module.exports = function registrationAction(provider) {
const management = instance(provider).configuration('features.registrationManagement');
if (management.rotateRegistrationAccessToken) {
ctx.oidc.entity('RotatedRegistrationAccessToken', ctx.oidc.entities.RegistrationAccessToken);
const rat = new provider.RegistrationAccessToken({ clientId: ctx.oidc.client.clientId });
const rat = new provider.RegistrationAccessToken({ client: ctx.oidc.client });

await ctx.oidc.registrationAccessToken.destroy();

Expand Down
19 changes: 18 additions & 1 deletion lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -664,8 +664,25 @@ const DEFAULTS = {
/*
* ttl
*
* description: Expirations (in seconds) for all token types
* description: Expirations (in seconds, or dynamically returned value) for all token types
* affects: tokens
*
* example: To resolve a ttl on runtime for each new token
* Configure `ttl` for a given token type with a function like so, this must return a value, not a
* Promise.
*
* ```js
* {
* ttl: {
* AccessToken(token, client) {
* // return a Number (in seconds) for the given token (first argument), the associated client is
* // passed as a second argument
* // Tip: if the values are entirely client based memoize the results
* return resolveTTLfor(token, client);
* },
* },
* }
* ```
*/
ttl: {
AccessToken: 60 * 60, // 1 hour in seconds
Expand Down
25 changes: 23 additions & 2 deletions lib/models/base_token.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,24 @@ module.exports = function getBaseToken(provider) {
this.jti = jti;
}

static get expiresIn() { return instance(provider).configuration(`ttl.${this.name}`); }
set client(client) {
this.clientId = client.clientId;
instance(this).client = client;
}

static expiresIn(...args) {
const ttl = instance(provider).configuration(`ttl.${this.name}`);

if (typeof ttl === 'number') {
return ttl;
}

if (typeof ttl === 'function') {
return ttl(...args);
}

return undefined;
}

get isValid() { return !this.isExpired; }

Expand Down Expand Up @@ -115,7 +132,11 @@ module.exports = function getBaseToken(provider) {
*
*/
get expiration() {
return this.expiresIn || this.constructor.expiresIn;
if (!this.expiresIn) {
this.expiresIn = this.constructor.expiresIn(this, instance(this).client);
}

return this.expiresIn;
}
}

Expand Down
12 changes: 10 additions & 2 deletions lib/models/id_token.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ module.exports = function getIdToken(provider) {
this.sector = sector;
}

static get expiresIn() { return instance(provider).configuration(`ttl.${this.name}`); }
static expiresIn(...args) {
const ttl = instance(provider).configuration(`ttl.${this.name}`);

if (typeof ttl === 'number') {
return ttl;
}

return ttl(...args);
}

set(key, value) { this.extra[key] = value; }

Expand Down Expand Up @@ -90,7 +98,7 @@ module.exports = function getIdToken(provider) {
signOptions = {
authorizedParty: audiences ? client.clientId : undefined,
audience: audiences ? ensureConform(audiences, client.clientId) : client.clientId,
expiresIn: noExp ? undefined : (expiresIn || this.constructor.expiresIn),
expiresIn: noExp ? undefined : (expiresIn || this.constructor.expiresIn(this, client)),
issuer: provider.issuer,
subject: payload.sub,
};
Expand Down
5 changes: 3 additions & 2 deletions test/authorization_code/code.grant.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,9 @@ describe('grant_type=authorization_code', () => {

context('', () => {
before(function () {
this.prev = this.provider.AuthorizationCode.expiresIn;
i(this.provider).configuration('ttl').AuthorizationCode = 5;
const ttl = i(this.provider).configuration('ttl');
this.prev = ttl.AuthorizationCode;
ttl.AuthorizationCode = 5;
});

after(function () {
Expand Down
2 changes: 1 addition & 1 deletion test/device_code/device_authorization_endpoint.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ describe('device_authorization_endpoint', () => {
]);
expect(body.verification_uri_complete).to.equal(`${body.verification_uri}?user_code=${body.user_code}`);
expect(body).to.have.property('verification_uri').that.matches(/\/device$/);
expect(body).to.have.property('expires_in', this.provider.DeviceCode.expiresIn);
expect(body).to.have.property('expires_in', 600);
response = body;
});

Expand Down
5 changes: 3 additions & 2 deletions test/device_code/device_code_grant.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,9 @@ describe('grant_type=urn:ietf:params:oauth:grant-type:device_code', () => {

context('', () => {
before(function () {
this.prev = this.provider.DeviceCode.expiresIn;
i(this.provider).configuration('ttl').DeviceCode = 0;
const ttl = i(this.provider).configuration('ttl');
this.prev = ttl.DeviceCode;
ttl.DeviceCode = 0;
});

after(function () {
Expand Down
26 changes: 26 additions & 0 deletions test/dynamic_token_ttl/dynamic_token_ttl.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { cloneDeep } = require('lodash');

const config = cloneDeep(require('../default.config'));

config.features = {
clientCredentials: true,
deviceCode: true,
alwaysIssueRefresh: true,
};

module.exports = {
config,
client: {
client_id: 'client',
token_endpoint_auth_method: 'none',
grant_types: [
'client_credentials',
'authorization_code',
'implicit',
'refresh_token',
'urn:ietf:params:oauth:grant-type:device_code',
],
response_types: ['code', 'code id_token token'],
redirect_uris: ['https://rp.example.com/cb'],
},
};
Loading

0 comments on commit 6788b83

Please sign in to comment.