Skip to content

Commit

Permalink
feat(JAR): add a helper allowing custom JWT claim and header validations
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed May 20, 2023
1 parent 56641ec commit be9242a
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 42 deletions.
47 changes: 47 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,7 @@ Enables the use and validations of the `request` and/or `request_uri` parameters
_**default value**_:
```js
{
assertJwtClaimsAndHeader: [AsyncFunction: assertJwtClaimsAndHeader], // see expanded details below
mode: 'strict',
request: false,
requestUri: false,
Expand All @@ -1558,6 +1559,52 @@ _**default value**_:
<details><summary>(Click to expand) features.requestObjects options details</summary><br>


#### assertJwtClaimsAndHeader

Helper function used to validate the Request Object JWT Claims Set and Header beyond what the JAR specification requires.


_**default value**_:
```js
async function assertJwtClaimsAndHeader(ctx, claims, header, client) {
// @param ctx - koa request context
// @param claims - parsed Request Object JWT Claims Set as object
// @param header - parsed Request Object JWT Headers as object
// @param client - the Client instance
const fapiProfile = ctx.oidc.isFapi('1.0 Final', '1.0 ID2', '2.0');
if (fapiProfile) {
if (!('exp' in claims)) {
throw new errors.InvalidRequestObject("Request Object is missing the 'exp' claim");
}
if (fapiProfile === '1.0 Final' || fapiProfile === '2.0') {
if (!('aud' in claims)) {
throw new errors.InvalidRequestObject("Request Object is missing the 'aud' claim");
}
if (!('nbf' in claims)) {
throw new errors.InvalidRequestObject("Request Object is missing the 'nbf' claim");
}
const diff = claims.exp - claims.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new errors.InvalidRequestObject("Request Object 'exp' claim too far from 'nbf' claim");
}
}
}
if (ctx.oidc.route === 'backchannel_authentication') {
for (const claim of ['exp', 'iat', 'nbf', 'jti']) {
if (!(claim in claims)) {
throw new errors.InvalidRequestObject(`Request Object is missing the '${claim}' claim`);
}
}
if (fapiProfile) {
const diff = claims.exp - claims.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new errors.InvalidRequestObject("Request Object 'exp' claim too far from 'nbf' claim");
}
}
}
}
```

#### mode

defines the provider's strategy when it comes to using regular OAuth 2.0 parameters that are present. Parameters inside the Request Object are ALWAYS used, this option controls whether to combine those with the regular ones or not.
Expand Down
56 changes: 14 additions & 42 deletions lib/actions/authorization/process_request_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,40 +159,12 @@ export default async function processRequestObject(PARAM_LIST, rejectDupesMiddle
ignoreAzp: true,
};

const fapiProfile = ctx.oidc.isFapi('1.0 Final', '1.0 ID2', '2.0');
if (fapiProfile) {
if (!('exp' in payload)) {
throw new InvalidRequestObject("Request Object is missing the 'exp' claim");
}

if (fapiProfile === '1.0 Final' || fapiProfile === '2.0') {
if (!('aud' in payload)) {
throw new InvalidRequestObject("Request Object is missing the 'aud' claim");
}
if (!('nbf' in payload)) {
throw new InvalidRequestObject("Request Object is missing the 'nbf' claim");
}
const diff = payload.exp - payload.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new InvalidRequestObject("Request Object 'exp' claim too far from 'nbf' claim");
}
}
}

if (isBackchannelAuthentication) {
for (const claim of ['exp', 'iat', 'nbf', 'jti']) {
if (!(claim in payload)) {
throw new InvalidRequestObject(`Request Object is missing the '${claim}' claim`);
}
}

if (fapiProfile) {
const diff = payload.exp - payload.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new InvalidRequestObject("Request Object 'exp' claim too far from 'nbf' claim");
}
}
}
await conf.features.requestObjects.assertJwtClaimsAndHeader(
ctx,
structuredClone(decoded.payload),
structuredClone(decoded.header),
client,
);

try {
JWT.assertPayload(payload, opts);
Expand Down Expand Up @@ -253,14 +225,10 @@ export default async function processRequestObject(PARAM_LIST, rejectDupesMiddle

params.request = undefined;

const mode = isBackchannelAuthentication || fapiProfile ? 'strict' : features.requestObjects.mode;

switch (mode) {
case 'lax':
// use all values from OAuth 2.0 unless they're in the Request Object
Object.assign(params, request);
break;
case 'strict':
switch (true) {
case features.requestObjects.mode === 'strict':
case isBackchannelAuthentication:
case !!ctx.oidc.fapiProfile:
Object.keys(params).forEach((key) => {
if (key in request) {
// use value from Request Object
Expand All @@ -271,6 +239,10 @@ export default async function processRequestObject(PARAM_LIST, rejectDupesMiddle
}
});
break;
case features.requestObjects.mode === 'lax':
// use all values from OAuth 2.0 unless they're in the Request Object
Object.assign(params, request);
break;
default:
}

Expand Down
49 changes: 49 additions & 0 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,47 @@ async function triggerAuthenticationDevice(ctx, request, account, client) {
throw new Error('features.ciba.triggerAuthenticationDevice not implemented');
}

async function assertJwtClaimsAndHeader(ctx, claims, header, client) {
// @param ctx - koa request context
// @param claims - parsed Request Object JWT Claims Set as object
// @param header - parsed Request Object JWT Headers as object
// @param client - the Client instance
const fapiProfile = ctx.oidc.isFapi('1.0 Final', '1.0 ID2', '2.0');
if (fapiProfile) {
if (!('exp' in claims)) {
throw new errors.InvalidRequestObject("Request Object is missing the 'exp' claim");
}

if (fapiProfile === '1.0 Final' || fapiProfile === '2.0') {
if (!('aud' in claims)) {
throw new errors.InvalidRequestObject("Request Object is missing the 'aud' claim");
}
if (!('nbf' in claims)) {
throw new errors.InvalidRequestObject("Request Object is missing the 'nbf' claim");
}
const diff = claims.exp - claims.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new errors.InvalidRequestObject("Request Object 'exp' claim too far from 'nbf' claim");
}
}
}

if (ctx.oidc.route === 'backchannel_authentication') {
for (const claim of ['exp', 'iat', 'nbf', 'jti']) {
if (!(claim in claims)) {
throw new errors.InvalidRequestObject(`Request Object is missing the '${claim}' claim`);
}
}

if (fapiProfile) {
const diff = claims.exp - claims.nbf;
if (Math.sign(diff) !== 1 || diff > 3600) {
throw new errors.InvalidRequestObject("Request Object 'exp' claim too far from 'nbf' claim");
}
}
}
}

function makeDefaults() {
const defaults = {

Expand Down Expand Up @@ -1685,6 +1726,14 @@ function makeDefaults() {
*
*/
mode: 'strict',

/**
* features.requestObjects.assertJwtClaimsAndHeader
*
* description: Helper function used to validate the Request Object JWT Claims Set and Header beyond
* what the JAR specification requires.
*/
assertJwtClaimsAndHeader,
},

/*
Expand Down

0 comments on commit be9242a

Please sign in to comment.