diff --git a/docs/README.md b/docs/README.md
index 55f14c336..53958342e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -2096,6 +2096,11 @@ _**default value**_:
{
AccessToken: 'opaque',
ClientCredentials: 'opaque',
+ customizers: {
+ 'jwt-ietf': undefined,
+ jwt: undefined,
+ paseto: undefined
+ },
jwtAccessTokenSigningAlg: [AsyncFunction: jwtAccessTokenSigningAlg]
}
```
@@ -2140,6 +2145,99 @@ server Configure `formats`:
```
+### formats.customizers
+
+helper function used by the OP before signing a structured Access Token of a given type, such as a JWT or PASETO one. Customizing here only changes the structured Access Token, not your storage, introspection or anything else. For such extras use [`extraAccessTokenClaims`](#extraaccesstokenclaims) instead.
+
+
+
+_**default value**_:
+```js
+{
+ 'jwt-ietf': undefined,
+ jwt: undefined,
+ paseto: undefined
+}
+```
+
+ (Click to expand) To push additional claims to a `jwt` format Access Token payload
+
+
+
+```js
+{
+ customizers: {
+ jwt(ctx, token, jwt) {
+ jwt.payload.foo = 'bar';
+ }
+ }
+}
+```
+
+
+ (Click to expand) To push additional headers to a `jwt` format Access Token
+
+
+
+```js
+{
+ customizers: {
+ jwt(ctx, token, jwt) {
+ jwt.header = { foo: 'bar' };
+ }
+ }
+}
+```
+
+
+ (Click to expand) To push additional claims to a `jwt-ietf` format Access Token payload
+
+
+
+```js
+{
+ customizers: {
+ ['jwt-ietf'](ctx, token, jwt) {
+ jwt.payload.foo = 'bar';
+ }
+ }
+}
+```
+
+
+ (Click to expand) To push additional headers to a `jwt-ietf` format Access Token
+
+
+
+```js
+{
+ customizers: {
+ ['jwt-ietf'](ctx, token, jwt) {
+ jwt.header = { foo: 'bar' };
+ }
+ }
+}
+```
+
+
+ (Click to expand) To push a payload and a footer to a PASETO structured access token
+
+
+
+```js
+{
+ customizers: {
+ paseto(ctx, token, structuredToken) {
+ structuredToken.payload.foo = 'bar';
+ structuredToken.footer = 'foo'
+ structuredToken.footer = Buffer.from('foo')
+ structuredToken.footer = { foo: 'bar' } // will get stringified
+ }
+ }
+}
+```
+
+
### formats.jwtAccessTokenSigningAlg
helper used by the provider to resolve a JWT Access Token signing algorithm. The resolved algorithm must be an asymmetric one supported by the provider's keys in jwks.
diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js
index dd3b9e8b6..7292b715d 100644
--- a/lib/helpers/defaults.js
+++ b/lib/helpers/defaults.js
@@ -1318,6 +1318,78 @@ const DEFAULTS = {
},
AccessToken: 'opaque',
ClientCredentials: 'opaque',
+
+ /*
+ * formats.customizers
+ *
+ * description: helper function used by the OP before signing a structured Access Token of a
+ * given type, such as a JWT or PASETO one. Customizing here only changes the structured Access
+ * Token, not your storage, introspection or anything else. For such extras use
+ * [`extraAccessTokenClaims`](#extraaccesstokenclaims) instead.
+ *
+ * example: To push additional claims to a `jwt` format Access Token payload
+ * ```js
+ * {
+ * customizers: {
+ * jwt(ctx, token, jwt) {
+ * jwt.payload.foo = 'bar';
+ * }
+ * }
+ * }
+ * ```
+ *
+ * example: To push additional headers to a `jwt` format Access Token
+ * ```js
+ * {
+ * customizers: {
+ * jwt(ctx, token, jwt) {
+ * jwt.header = { foo: 'bar' };
+ * }
+ * }
+ * }
+ * ```
+ *
+ * example: To push additional claims to a `jwt-ietf` format Access Token payload
+ * ```js
+ * {
+ * customizers: {
+ * ['jwt-ietf'](ctx, token, jwt) {
+ * jwt.payload.foo = 'bar';
+ * }
+ * }
+ * }
+ * ```
+ *
+ * example: To push additional headers to a `jwt-ietf` format Access Token
+ * ```js
+ * {
+ * customizers: {
+ * ['jwt-ietf'](ctx, token, jwt) {
+ * jwt.header = { foo: 'bar' };
+ * }
+ * }
+ * }
+ * ```
+ *
+ * example: To push a payload and a footer to a PASETO structured access token
+ * ```js
+ * {
+ * customizers: {
+ * paseto(ctx, token, structuredToken) {
+ * structuredToken.payload.foo = 'bar';
+ * structuredToken.footer = 'foo'
+ * structuredToken.footer = Buffer.from('foo')
+ * structuredToken.footer = { foo: 'bar' } // will get stringified
+ * }
+ * }
+ * }
+ * ```
+ */
+ customizers: {
+ jwt: undefined,
+ 'jwt-ietf': undefined,
+ paseto: undefined,
+ },
},
/*
diff --git a/lib/helpers/jwt.js b/lib/helpers/jwt.js
index 46fd54229..7b5f547ed 100644
--- a/lib/helpers/jwt.js
+++ b/lib/helpers/jwt.js
@@ -27,7 +27,7 @@ function verifyAudience({ aud, azp }, expected, checkAzp) {
class JWT {
// TODO: this does not need to be async anymore
static async sign(payload, key, alg, options = {}) {
- const header = { alg, typ: options.typ !== undefined ? options.typ : typ };
+ const header = { ...options.fields, alg, typ: options.typ !== undefined ? options.typ : typ };
const timestamp = epochTime();
const iat = options.noIat ? undefined : timestamp;
diff --git a/lib/helpers/paseto.js b/lib/helpers/paseto.js
index 25b892f18..b303bb0c5 100644
--- a/lib/helpers/paseto.js
+++ b/lib/helpers/paseto.js
@@ -27,7 +27,13 @@ const pae = (...pieces) => {
return accumulator;
};
-const pack = (header, payload) => `${header}${base64url.encodeBuffer(Buffer.concat(payload))}`;
+const pack = (header, payload, footer) => {
+ if (footer.length !== 0) {
+ return `${header}${base64url.encodeBuffer(Buffer.concat(payload))}.${base64url.encodeBuffer(footer)}`;
+ }
+
+ return `${header}${base64url.encodeBuffer(Buffer.concat(payload))}`;
+};
const decode = (paseto) => {
@@ -42,11 +48,21 @@ const decode = (paseto) => {
return JSON.parse(base64url.decodeToBuffer(sPayload).slice(0, -64));
};
-const sign = (payload, key) => {
+const sign = ({ payload, footer }, key) => {
const h = `${VERSION}.${PURPOSE}.`;
const m = Buffer.from(JSON.stringify(payload), 'utf8');
- const sig = signOneShot(undefined, pae(h, m, ''), key);
- return pack(h, [m, sig]);
+ let f;
+ if (typeof footer === 'string') {
+ f = Buffer.from(footer, 'utf8');
+ } else if (Buffer.isBuffer(footer)) {
+ f = footer;
+ } else if (footer) {
+ f = Buffer.from(JSON.stringify(footer));
+ } else {
+ f = Buffer.from('');
+ }
+ const sig = signOneShot(undefined, pae(h, m, f), key);
+ return pack(h, [m, sig], f);
};
module.exports = { sign, decode };
diff --git a/lib/models/formats/jwt.js b/lib/models/formats/jwt.js
index 68fe330b6..09e96f996 100644
--- a/lib/models/formats/jwt.js
+++ b/lib/models/formats/jwt.js
@@ -85,7 +85,19 @@ module.exports = (provider, { opaque }) => {
tokenPayload.cnf['jkt#S256'] = jkt;
}
- value = await JWT.sign(tokenPayload, key, alg);
+ const structuredToken = {
+ header: undefined,
+ payload: tokenPayload,
+ };
+
+ const customizer = instance(provider).configuration('formats.customizers.jwt');
+ if (customizer) {
+ await customizer(ctx, this, structuredToken);
+ }
+
+ value = await JWT.sign(structuredToken.payload, key, alg, {
+ fields: structuredToken.header,
+ });
}
payload.jwt = value;
diff --git a/lib/models/formats/jwt_ietf.js b/lib/models/formats/jwt_ietf.js
index dc20c177e..07e93356b 100644
--- a/lib/models/formats/jwt_ietf.js
+++ b/lib/models/formats/jwt_ietf.js
@@ -62,7 +62,19 @@ module.exports = (provider, { opaque, jwt }) => ({
tokenPayload.cnf['jkt#S256'] = jkt;
}
- value = await JWT.sign(tokenPayload, key, alg, { typ: 'at+jwt' });
+ const structuredToken = {
+ header: undefined,
+ payload: tokenPayload,
+ };
+
+ const customizer = instance(provider).configuration('formats.customizers.jwt-ietf');
+ if (customizer) {
+ await customizer(ctx, this, structuredToken);
+ }
+
+ value = await JWT.sign(structuredToken.payload, key, alg, {
+ typ: 'at+jwt', fields: structuredToken.header,
+ });
}
payload[PROPERTY] = value;
diff --git a/lib/models/formats/paseto.js b/lib/models/formats/paseto.js
index 8584f83c9..4034c2680 100644
--- a/lib/models/formats/paseto.js
+++ b/lib/models/formats/paseto.js
@@ -53,12 +53,13 @@ module.exports = (provider, { opaque }) => {
getSigningKey();
}
+ const ctx = ctxRef.get(this);
+
if (sub) {
// TODO: in v7.x require token.client to be set
const client = this.client || await provider.Client.find(clientId);
assert(client && client.clientId === clientId);
if (client.sectorIdentifier) {
- const ctx = ctxRef.get(this);
const pairwiseIdentifier = instance(provider).configuration('pairwiseIdentifier');
sub = await pairwiseIdentifier(ctx, sub, client);
}
@@ -86,7 +87,17 @@ module.exports = (provider, { opaque }) => {
tokenPayload.cnf['jkt#S256'] = jkt;
}
- value = await paseto.sign(tokenPayload, key);
+ const structuredToken = {
+ payload: tokenPayload,
+ footer: undefined,
+ };
+
+ const customizer = instance(provider).configuration('formats.customizers.paseto');
+ if (customizer) {
+ await customizer(ctx, this, structuredToken);
+ }
+
+ value = await paseto.sign(structuredToken, key);
}
payload.paseto = value;
diff --git a/test/formats/paseto.test.js b/test/formats/paseto.test.js
index 147c15293..fc672cc4b 100644
--- a/test/formats/paseto.test.js
+++ b/test/formats/paseto.test.js
@@ -1,19 +1,17 @@
/* eslint-disable no-param-reassign */
+const { createPublicKey } = require('crypto');
+
const { spy, match: { string, number }, assert } = require('sinon');
const { expect } = require('chai');
-const base64url = require('base64url');
-const pasetoLib = require('paseto');
const { formats: { AccessToken: FORMAT } } = require('../../lib/helpers/defaults');
const epochTime = require('../../lib/helpers/epoch_time');
const bootstrap = require('../test_helper');
-function decode(paseto) {
- return JSON.parse(base64url.toBuffer(paseto.split('.')[2]).slice(0, -64));
-}
-
if (FORMAT === 'paseto') {
+ const pasetoLib = require('paseto'); // eslint-disable-line global-require
+ const key = createPublicKey(global.keystore.get({ kty: 'OKP' }).toPEM(false));
describe('paseto storage', () => {
before(bootstrap(__dirname));
const accountId = 'account';
@@ -97,7 +95,7 @@ if (FORMAT === 'paseto') {
});
const { iat, jti, exp } = upsert.getCall(0).args[1];
- const payload = decode(paseto);
+ const payload = await pasetoLib.V2.verify(paseto, key);
expect(payload).to.eql({
...extra,
aud,
@@ -145,7 +143,7 @@ if (FORMAT === 'paseto') {
});
const { iat, jti, exp } = upsert.getCall(0).args[1];
- const payload = decode(paseto);
+ const payload = await pasetoLib.V2.verify(paseto, key);
expect(payload).to.eql({
...extra,
aud,
@@ -185,7 +183,7 @@ if (FORMAT === 'paseto') {
});
const { iat, jti, exp } = upsert.getCall(0).args[1];
- const payload = decode(paseto);
+ const payload = await pasetoLib.V2.verify(paseto, key);
expect(payload).to.eql({
...extra,
aud,
@@ -218,7 +216,7 @@ if (FORMAT === 'paseto') {
};
let paseto = await accessToken.save();
- const { payload } = pasetoLib.decode(paseto);
+ const { payload } = await pasetoLib.V2.verify(paseto, key, { complete: true });
expect(payload).to.have.property('customized', true);
i(this.provider).configuration('formats.customizers').paseto = (ctx, token, t) => {
@@ -227,7 +225,7 @@ if (FORMAT === 'paseto') {
};
paseto = await accessToken.save();
- let { footer } = pasetoLib.decode(paseto);
+ let { footer } = await pasetoLib.V2.verify(paseto, key, { complete: true });
expect(footer).to.be.instanceOf(Buffer);
expect(JSON.parse(footer)).to.have.property('customized', true);
@@ -236,7 +234,7 @@ if (FORMAT === 'paseto') {
};
paseto = await accessToken.save();
- ({ footer } = pasetoLib.decode(paseto));
+ ({ footer } = await pasetoLib.V2.verify(paseto, key, { complete: true }));
expect(footer).to.be.instanceOf(Buffer);
expect(footer.toString()).to.eql('foobar');
@@ -245,7 +243,7 @@ if (FORMAT === 'paseto') {
};
paseto = await accessToken.save();
- ({ footer } = pasetoLib.decode(paseto));
+ ({ footer } = await pasetoLib.V2.verify(paseto, key, { complete: true }));
expect(footer).to.be.instanceOf(Buffer);
expect(footer.toString()).to.eql('foobarbaz');
});