Skip to content

Commit

Permalink
refactor: preparation for removal of structured tokens from db
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Mar 11, 2020
1 parent 6afaf31 commit 4df1a0c
Show file tree
Hide file tree
Showing 17 changed files with 78 additions and 96 deletions.
14 changes: 7 additions & 7 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2271,11 +2271,11 @@ _**default value**_:

### formats

This option allows to configure the token serialization format. The different values change how a client-facing token value is generated as well as what properties get sent to the adapter for storage.
- `opaque` (default) formatted tokens store every property as a root property in your adapter
- `jwt` formatted tokens are issued as JWTs and stored the same as `opaque` only with additional property `jwt`. See `formats.jwtAccessTokenSigningAlg` for resolving the JWT Access Token signing algorithm. Note this is a proprietary format that will eventually get deprecated in favour of the 'jwt-ietf' value (once it gets stable and close to being an RFC)
- `jwt-ietf` formatted tokens are issued as JWTs and stored the same as `opaque` only with additional property `jwt-ietf`. See `formats.jwtAccessTokenSigningAlg` for resolving the JWT Access Token signing algorithm. This is an implementation of [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-05) draft and to enable it you need to enable `features.ietfJWTAccessTokenProfile`. 'jwt-ietf' value (once it gets stable and close to being an RFC)
- the value may also be a function dynamically determining the format (returning either `jwt`, `jwt-ietf`, or `opaque` depending on the token itself)
This option allows to configure the token value format. The different values change how a client-facing token value is generated.
Supported formats are:
- `opaque` (default) tokens are PRNG generated random strings using url safe base64 alphabet. See `formats.bitsOfOpaqueRandomness` for influencing the token length.
- `jwt` tokens are issued as JWTs. See `formats.tokenSigningAlg` for resolving the JWT Access Token signing algorithm. Note this is a proprietary format that will eventually get deprecated in favour of the 'jwt-ietf' value (once it gets stable and close to being an RFC).
- `jwt-ietf` tokens are issued as JWTs. See `formats.tokenSigningAlg` for resolving the JWT Access Token signing algorithm. This is an implementation of [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-05) draft and to enable it you need to enable `features.ietfJWTAccessTokenProfile`.



Expand All @@ -2301,10 +2301,10 @@ Configure `formats`:
{ AccessToken: 'jwt' }
```
</details>
<a id="formats-to-dynamically-decide-on-the-format-used-e-g-only-if-it-is-intended-for-a-resource"></a><details><summary>(Click to expand) To dynamically decide on the format used, e.g. only if it is intended for a resource</summary><br>
<a id="formats-to-dynamically-decide-on-the-format-used"></a><details><summary>(Click to expand) To dynamically decide on the format used</summary><br>


server Configure `formats`:
Configure `formats`:


```js
Expand Down
9 changes: 0 additions & 9 deletions example/my_adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,6 @@ class MyAdapter {
* - policies {string[]} - [InitialAccessToken, RegistrationAccessToken only] array of policies
* - request {string} - [PushedAuthorizationRequest only] Pushed Request Object value
*
*
* when `jwt`
* - same as `opaque` with the addition of
* - jwt {string} - the JWT value returned to the client
*
* when `jwt-ietf`
* - same as `opaque` with the addition of
* - 'jwt-ietf' {string} - the JWT value returned to the client
*
* Client model will only use this when registered through Dynamic Registration features and
* will contain all client properties.
*
Expand Down
27 changes: 11 additions & 16 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -1635,24 +1635,20 @@ function getDefaults() {
/*
* formats
*
* description: This option allows to configure the token serialization format. The different
* values change how a client-facing token value is generated as well as what properties get
* sent to the adapter for storage.
* - `opaque` (default) formatted tokens store every property as a root property in your adapter
* - `jwt` formatted tokens are issued as JWTs and stored the same as `opaque` only with
* additional property `jwt`. See `formats.jwtAccessTokenSigningAlg` for resolving the JWT
* description: This option allows to configure the token value format. The different
* values change how a client-facing token value is generated.
*
* Supported formats are:
* - `opaque` (default) tokens are PRNG generated random strings using url safe base64 alphabet.
* See `formats.bitsOfOpaqueRandomness` for influencing the token length.
* - `jwt` tokens are issued as JWTs. See `formats.tokenSigningAlg` for resolving the JWT
* Access Token signing algorithm.
* Note this is a proprietary format that will eventually get deprecated in favour of the
* 'jwt-ietf' value (once it gets stable and close to being an RFC)
* - `jwt-ietf` formatted tokens are issued as JWTs and stored the same as `opaque` only with
* additional property `jwt-ietf`. See `formats.jwtAccessTokenSigningAlg` for resolving the
* JWT Access Token signing algorithm.
* This is an implementation of
* 'jwt-ietf' value (once it gets stable and close to being an RFC).
* - `jwt-ietf` tokens are issued as JWTs. See `formats.tokenSigningAlg` for resolving the JWT
* Access Token signing algorithm. This is an implementation of
* [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-05)
* draft and to enable it you need to enable `features.ietfJWTAccessTokenProfile`.
* 'jwt-ietf' value (once it gets stable and close to being an RFC)
* - the value may also be a function dynamically determining the format (returning either
* `jwt`, `jwt-ietf`, or `opaque` depending on the token itself)
*
* example: To enable JWT Access Tokens
*
Expand All @@ -1661,8 +1657,7 @@ function getDefaults() {
* { AccessToken: 'jwt' }
* ```
*
* example: To dynamically decide on the format used, e.g. only if it is intended for a resource
* server
* example: To dynamically decide on the format used
*
* Configure `formats`:
* ```js
Expand Down
6 changes: 1 addition & 5 deletions lib/helpers/jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class JWT {

static assertPayload(payload, {
clockTolerance = 0, audience, ignoreExpiration,
ignoreAzp, ignoreIssued, ignoreNotBefore, issuer, jti,
ignoreAzp, ignoreIssued, ignoreNotBefore, issuer,
} = {}) {
const timestamp = epochTime();

Expand Down Expand Up @@ -114,10 +114,6 @@ class JWT {
assert.equal(typeof payload.iss, 'string', 'invalid iss value');
}

if (jti) {
assert.equal(payload.jti, jti, 'jwt jti invalid');
}

if (audience) {
verifyAudience(
payload,
Expand Down
1 change: 1 addition & 0 deletions lib/helpers/symbols.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
module.exports = {
ROUTER_URL_METHOD: Symbol('ROUTER_URL_METHOD'),
LOOKUP_VALUE: Symbol('LOOKUP_VALUE'),
};
13 changes: 11 additions & 2 deletions lib/models/base_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const snakeCase = require('../helpers/_/snake_case');
const epochTime = require('../helpers/epoch_time');
const pickBy = require('../helpers/_/pick_by');
const instance = require('../helpers/weak_cache');
const { LOOKUP_VALUE } = require('../helpers/symbols');

const hasFormat = require('./mixins/has_format');

Expand Down Expand Up @@ -42,6 +43,14 @@ module.exports = function getBaseToken(provider) {
this.jti = jti;
}

static instantiate(payload, lookupValue) {
const model = new this(payload);
if (lookupValue) {
model[LOOKUP_VALUE] = lookupValue;
}
return model;
}

async save(ttl) {
if (!this.jti) {
this.jti = this.generateTokenId();
Expand Down Expand Up @@ -92,10 +101,10 @@ module.exports = function getBaseToken(provider) {

try {
assert(stored);
const payload = await this.verify(value, stored, { ignoreExpiration });
const payload = await this.verify(stored, { ignoreExpiration });
assert.equal(jti, payload.jti);

return new this(payload);
return this.instantiate(payload, value);
} catch (err) {
return undefined;
}
Expand Down
6 changes: 2 additions & 4 deletions lib/models/device_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ module.exports = (provider) => class DeviceCode extends apply([
try {
assert(stored);
assert.equal(userCode, stored.userCode);
const payload = await this.verify(undefined, stored, {
ignoreExpiration, foundByReference: true,
});
return new this(payload);
const payload = await this.verify(stored, { ignoreExpiration });
return this.instantiate(payload);
} catch (err) {
return undefined;
}
Expand Down
14 changes: 4 additions & 10 deletions lib/models/formats/dynamic.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const { strict: assert } = require('assert');

const JWT = require('../../helpers/jwt');
const instance = require('../../helpers/weak_cache');
const ctxRef = require('../ctx_ref');

Expand All @@ -22,22 +21,17 @@ module.exports = (provider, formats) => ({
getTokenId(...args) {
let format;
const [value] = args;
if (value && (value.length === 27 || value.length === 43)) {
format = 'opaque';
} else if (JWT_REGEX.test(value)) {
if (JWT.header(value).typ === 'at+jwt') {
format = 'jwt-ietf';
} else {
format = 'jwt';
}
if (JWT_REGEX.test(value)) {
// get tokenId is the same for jwt and jwt-ietf
format = 'jwt';
} else {
format = 'opaque';
}
assert(formats[format] && format !== 'dynamic', 'invalid format resolved');
return formats[format].getTokenId.apply(this, args);
},
async verify(...args) {
const format = args[1].format || (args[1].jwt ? 'jwt' : args[1]['jwt-ietf'] ? 'jwt-ietf' : 'opaque'); // eslint-disable-line no-nested-ternary
const { format } = args[0];
assert(formats[format] && format !== 'dynamic', 'invalid format resolved');
return formats[format].verify.apply(this, args);
},
Expand Down
19 changes: 9 additions & 10 deletions lib/models/formats/jwt.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
const { strict: assert } = require('assert');

const { JWS } = require('jose');

const JWT = require('../../helpers/jwt');
const instance = require('../../helpers/weak_cache');
const nanoid = require('../../helpers/nanoid');
const base64url = require('../../helpers/base64url');
const { LOOKUP_VALUE } = require('../../helpers/symbols');
const ctxRef = require('../ctx_ref');

function getClaim(token, claim) {
Expand Down Expand Up @@ -50,8 +53,8 @@ module.exports = (provider, { opaque }) => {
let { accountId: sub } = payload;

let value;
if (this.jwt) {
value = this.jwt;
if (this[LOOKUP_VALUE]) {
value = this[LOOKUP_VALUE];
} else {
const ctx = ctxRef.get(this);
const { key, alg } = await getSigningAlgAndKey(ctx, this, azp);
Expand Down Expand Up @@ -98,20 +101,16 @@ module.exports = (provider, { opaque }) => {
fields: structuredToken.header,
});
}
payload.jwt = value;
payload.format = 'jwt';

return [value, payload];
},
getTokenId(token) {
JWS.verify(token, instance(provider).keystore, { parse: false });
return getClaim(token, 'jti');
},
async verify(token, stored, { ignoreExpiration, foundByReference }) {
let jti;
if (!foundByReference) {
assert.equal(token, stored.jwt);
jti = this.getTokenId(token);
}
return opaque.verify.call(this, jti, stored, { ignoreExpiration, foundByReference });
async verify(stored, { ignoreExpiration } = {}) {
return opaque.verify.call(this, stored, { ignoreExpiration, format: 'jwt' });
},
};
};
18 changes: 6 additions & 12 deletions lib/models/formats/jwt_ietf.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ const { strict: assert } = require('assert');

const instance = require('../../helpers/weak_cache');
const JWT = require('../../helpers/jwt');
const { LOOKUP_VALUE } = require('../../helpers/symbols');
const ctxRef = require('../ctx_ref');

const PROPERTY = 'jwt-ietf';

module.exports = (provider, { opaque, jwt }) => ({
generateTokenId: jwt.generateTokenId,
getTokenId: jwt.getTokenId,
Expand All @@ -25,8 +24,8 @@ module.exports = (provider, { opaque, jwt }) => ({
}

let value;
if (this[PROPERTY]) {
value = this[PROPERTY];
if (this[LOOKUP_VALUE]) {
value = this[LOOKUP_VALUE];
} else {
const ctx = ctxRef.get(this);
const { key, alg } = await jwt.getSigningAlgAndKey(ctx, this, clientId);
Expand Down Expand Up @@ -75,16 +74,11 @@ module.exports = (provider, { opaque, jwt }) => ({
typ: 'at+jwt', fields: structuredToken.header,
});
}
payload[PROPERTY] = value;
payload.format = 'jwt-ietf';

return [value, payload];
},
async verify(token, stored, { ignoreExpiration, foundByReference }) {
let jti;
if (!foundByReference) {
assert.equal(token, stored[PROPERTY]);
jti = this.getTokenId(token);
}
return opaque.verify.call(this, jti, stored, { ignoreExpiration, foundByReference });
async verify(stored, { ignoreExpiration } = {}) {
return opaque.verify.call(this, stored, { ignoreExpiration, format: 'jwt-ietf' });
},
});
7 changes: 5 additions & 2 deletions lib/models/formats/opaque.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { strict: assert } = require('assert');

const pickBy = require('../../helpers/_/pick_by');
const { assertPayload } = require('../../helpers/jwt');
const epochTime = require('../../helpers/epoch_time');
Expand All @@ -18,6 +20,7 @@ module.exports = (provider) => ({
const exp = this.exp || now + this.expiration;
const value = this.jti;
const payload = {
format: 'opaque',
iat: this.iat || epochTime(),
...(exp ? { exp } : undefined),
...pickBy(
Expand All @@ -35,11 +38,11 @@ module.exports = (provider) => ({
getTokenId(token) {
return token;
},
async verify(token, stored, { ignoreExpiration, foundByReference }) {
async verify(stored, { ignoreExpiration, format = 'opaque' } = {}) {
assert.equal(stored.format, format);
assertPayload(stored, {
ignoreExpiration,
clockTolerance: instance(provider).configuration('clockTolerance'),
...(foundByReference ? undefined : { jti: token }),
});

return stored;
Expand Down
8 changes: 0 additions & 8 deletions lib/models/mixins/has_format.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,6 @@ module.exports = (provider, type, superclass) => {
instance(provider).dynamic[type] = FORMAT;
}

const { IN_PAYLOAD } = klass.prototype.constructor;
Object.defineProperties(klass.prototype.constructor, {
format: { value: dynamic ? 'dynamic' : FORMAT },
...(FORMAT === 'jwt-ietf' ? { IN_PAYLOAD: { value: [...IN_PAYLOAD, 'jwt-ietf'] } } : undefined),
...(FORMAT === 'jwt' ? { IN_PAYLOAD: { value: [...IN_PAYLOAD, 'jwt'] } } : undefined),
...(dynamic ? { IN_PAYLOAD: { value: [...IN_PAYLOAD, 'format', 'jwt', 'jwt-ietf'] } } : undefined),
});

return klass;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/models/replay_detection.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ module.exports = (provider) => class ReplayDetection extends hasFormat(provider,
return false;
}

const inst = new this({
const inst = this.instantiate({
jti: id,
iss,
});
Expand Down
8 changes: 4 additions & 4 deletions lib/models/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ module.exports = function getSession(provider) {
const stored = await this.adapter.findByUid(uid);
try {
assert(stored);
const payload = await this.verify(undefined, stored, { foundByReference: true });
return new this(payload);
const payload = await this.verify(stored);
return this.instantiate(payload);
} catch (err) {
return undefined;
}
Expand Down Expand Up @@ -113,9 +113,9 @@ module.exports = function getSession(provider) {
// underlying session was removed since we have a session id in cookie, let's assign an
// empty data so that session.new is not true and cookie will get written even if nothing
// gets written to it
session = new this({});
session = this.instantiate({});
} else {
session = new this();
session = this.instantiate();
}
}

Expand Down
Loading

0 comments on commit 4df1a0c

Please sign in to comment.