Skip to content

Commit

Permalink
feat: allow structured access token customizations
Browse files Browse the repository at this point in the history
resolves #520
  • Loading branch information
panva committed Aug 29, 2019
1 parent 3ad1744 commit 4be3bb2
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 22 deletions.
98 changes: 98 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2096,6 +2096,11 @@ _**default value**_:
{
AccessToken: 'opaque',
ClientCredentials: 'opaque',
customizers: {
'jwt-ietf': undefined,
jwt: undefined,
paseto: undefined
},
jwtAccessTokenSigningAlg: [AsyncFunction: jwtAccessTokenSigningAlg]
}
```
Expand Down Expand Up @@ -2140,6 +2145,99 @@ server Configure `formats`:
```
</details>

### 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
}
```
<a name="formats-customizers-to-push-additional-claims-to-a-jwt-format-access-token-payload"></a><details>
<summary>(Click to expand) To push additional claims to a `jwt` format Access Token payload
</summary>
<br>

```js
{
customizers: {
jwt(ctx, token, jwt) {
jwt.payload.foo = 'bar';
}
}
}
```
</details>
<a name="formats-customizers-to-push-additional-headers-to-a-jwt-format-access-token"></a><details>
<summary>(Click to expand) To push additional headers to a `jwt` format Access Token
</summary>
<br>

```js
{
customizers: {
jwt(ctx, token, jwt) {
jwt.header = { foo: 'bar' };
}
}
}
```
</details>
<a name="formats-customizers-to-push-additional-claims-to-a-jwt-ietf-format-access-token-payload"></a><details>
<summary>(Click to expand) To push additional claims to a `jwt-ietf` format Access Token payload
</summary>
<br>

```js
{
customizers: {
['jwt-ietf'](ctx, token, jwt) {
jwt.payload.foo = 'bar';
}
}
}
```
</details>
<a name="formats-customizers-to-push-additional-headers-to-a-jwt-ietf-format-access-token"></a><details>
<summary>(Click to expand) To push additional headers to a `jwt-ietf` format Access Token
</summary>
<br>

```js
{
customizers: {
['jwt-ietf'](ctx, token, jwt) {
jwt.header = { foo: 'bar' };
}
}
}
```
</details>
<a name="formats-customizers-to-push-a-payload-and-a-footer-to-a-paseto-structured-access-token"></a><details>
<summary>(Click to expand) To push a payload and a footer to a PASETO structured access token
</summary>
<br>

```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
}
}
}
```
</details>

### 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.
Expand Down
72 changes: 72 additions & 0 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},

/*
Expand Down
2 changes: 1 addition & 1 deletion lib/helpers/jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 20 additions & 4 deletions lib/helpers/paseto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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 };
14 changes: 13 additions & 1 deletion lib/models/formats/jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
14 changes: 13 additions & 1 deletion lib/models/formats/jwt_ietf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
15 changes: 13 additions & 2 deletions lib/models/formats/paseto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;

Expand Down
Loading

0 comments on commit 4be3bb2

Please sign in to comment.