Skip to content

Commit

Permalink
feat: incorporate behaviours and metadata from jwsreq-25
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Jul 14, 2020
1 parent fd2ccee commit cb12761
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 10 deletions.
11 changes: 11 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,7 @@ _**default value**_:
},
request: false,
requestUri: true,
requireSignedRequestObject: false,
requireUriRegistration: true
}
```
Expand Down Expand Up @@ -1604,6 +1605,16 @@ _**default value**_:
true
```

#### requireSignedRequestObject

Makes the use of signed request objects required for all authorization requests as an OP policy.


_**default value**_:
```js
false
```

#### requireUriRegistration

Makes request_uri pre-registration mandatory (true) or optional (false).
Expand Down
5 changes: 4 additions & 1 deletion lib/actions/authorization/process_request_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ module.exports = async function processRequestObject(PARAM_LIST, rejectDupesMidd
}

if (
client.requestObjectSigningAlg
// TODO: in v7.x remove client.requestObjectSigningAlg as a prerequisite (breaking)
(client.requestObjectSigningAlg || client.requireSignedRequestObject)
&& params.request === undefined
) {
throw new InvalidRequest('Request Object must be used by this client');
Expand Down Expand Up @@ -184,6 +185,8 @@ module.exports = async function processRequestObject(PARAM_LIST, rejectDupesMidd
throw new InvalidRequestObject(`request replay detected (jti: ${payload.jti})`);
}
}
} else if (client.requireSignedRequestObject) {
throw new InvalidRequestObject('Request Object must not be unsigned for this client');
}

if (pushedRequestObject) {
Expand Down
17 changes: 10 additions & 7 deletions lib/actions/discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@ module.exports = function discovery(ctx, next) {
issuer: ctx.oidc.issuer,
jwks_uri: ctx.oidc.urlFor('jwks'),
registration_endpoint: features.registration.enabled ? ctx.oidc.urlFor('registration') : undefined,
request_object_signing_alg_values_supported:
features.requestObjects.request || features.requestObjects.requestUri
? config.requestObjectSigningAlgValues : undefined,
request_parameter_supported: features.requestObjects.request,
request_uri_parameter_supported: features.requestObjects.requestUri,
require_request_uri_registration: features.requestObjects.requestUri && features.requestObjects.requireUriRegistration ? true : undefined,
response_modes_supported: ['form_post', 'fragment', 'query'],
response_types_supported: config.responseTypes,
scopes_supported: [...config.scopes].concat([...config.dynamicScopes].map((s) => s[DYNAMIC_SCOPE_LABEL]).filter(Boolean)),
Expand All @@ -42,6 +36,15 @@ module.exports = function discovery(ctx, next) {
ctx.body.require_pushed_authorization_requests = features.pushedAuthorizationRequests.requirePushedAuthorizationRequests ? true : undefined;
}

const { requestObjects } = features;
if (requestObjects.request || requestObjects.requestUri) {
ctx.body.request_object_signing_alg_values_supported = config.requestObjectSigningAlgValues;
ctx.body.request_parameter_supported = requestObjects.request;
ctx.body.request_uri_parameter_supported = requestObjects.requestUri;
ctx.body.require_request_uri_registration = requestObjects.requireUriRegistration ? true : undefined;
ctx.body.require_signed_request_object = requestObjects.requireSignedRequestObject ? true : undefined;
}

if (features.userinfo.enabled) {
ctx.body.userinfo_endpoint = ctx.oidc.urlFor('userinfo');
if (features.jwtUserinfo.enabled) {
Expand Down Expand Up @@ -106,7 +109,7 @@ module.exports = function discovery(ctx, next) {
ctx.body.authorization_encryption_enc_values_supported = config.authorizationEncryptionEncValues;
}

if (features.requestObjects.request || features.requestObjects.requestUri) {
if (requestObjects.request || requestObjects.requestUri) {
ctx.body.request_object_encryption_alg_values_supported = config.requestObjectEncryptionAlgValues;
ctx.body.request_object_encryption_enc_values_supported = config.requestObjectEncryptionEncValues;
}
Expand Down
2 changes: 2 additions & 0 deletions lib/consts/client_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const DEFAULT = {
post_logout_redirect_uris: [],
backchannel_logout_session_required: false,
frontchannel_logout_session_required: false,
require_signed_request_object: false,
require_pushed_authorization_requests: false,
};

Expand All @@ -54,6 +55,7 @@ const BOOL = [
'frontchannel_logout_session_required',
'require_auth_time',
'tls_client_certificate_bound_access_tokens',
'require_signed_request_object',
'require_pushed_authorization_requests',
];

Expand Down
15 changes: 15 additions & 0 deletions lib/helpers/client_schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ module.exports = function getSchema(provider) {

if (features.requestObjects.request || features.requestObjects.requestUri) {
RECOGNIZED_METADATA.push('request_object_signing_alg');
RECOGNIZED_METADATA.push('require_signed_request_object');
if (features.encryption.enabled) {
RECOGNIZED_METADATA.push('request_object_encryption_alg');
RECOGNIZED_METADATA.push('request_object_encryption_enc');
Expand Down Expand Up @@ -253,6 +254,7 @@ module.exports = function getSchema(provider) {
this.webMessageUris();
this.checkContacts();
this.backchannelLogoutNeedsIdTokenAlg();
this.jarPolicy();
this.parPolicy();

// max_age and client_secret_expires_at format
Expand Down Expand Up @@ -589,6 +591,19 @@ module.exports = function getSchema(provider) {
}
}

jarPolicy() {
const jar = configuration.features.requestObjects;
const enabled = jar.request || jar.requestUri;
if (enabled) {
if (jar.requireSignedRequestObject) {
this.require_signed_request_object = true;
}
if (this.require_signed_request_object && this.request_object_signing_alg === 'none') {
this.invalidate('request_object_signing_alg must not be "none" when require_signed_request_object is true');
}
}
}

ensureStripUnrecognized() {
const whitelisted = [...RECOGNIZED_METADATA, ...configuration.extraClientMetadata.properties];
Object.keys(this).forEach((prop) => {
Expand Down
8 changes: 8 additions & 0 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,14 @@ function getDefaults() {
*/
requireUriRegistration: true,

/*
* features.requestObjects.requireSignedRequestObject
*
* description: Makes the use of signed request objects required for all authorization
* requests as an OP policy.
*/
requireSignedRequestObject: false,

mergingStrategy: {

/*
Expand Down
23 changes: 23 additions & 0 deletions test/configuration/client_metadata.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,29 @@ describe('Client metadata validation', () => {
rejects(this.title, ['not a member', '1'], "default_acr_values can only contain '0', '1', or '2'", undefined, { acrValues });
});

context('require_signed_request_object', function () {
const configuration = (value = false, requestUri = true) => ({
features: {
requestObjects: {
requestUri,
requireSignedRequestObject: value,
},
},
});
mustBeBoolean(this.title);
defaultsTo(this.title, undefined, undefined, configuration(false, false));
defaultsTo(this.title, false, undefined, configuration());
defaultsTo(this.title, true, undefined, configuration(true));
defaultsTo(this.title, true, {
require_signed_request_object: false,
}, configuration(true));
defaultsTo(this.title, true, undefined, {
...configuration(),
clientDefaults: { require_signed_request_object: true },
});
rejects(this.title, true, 'request_object_signing_alg must not be "none" when require_signed_request_object is true', { request_object_signing_alg: 'none' });
});

context('default_max_age', function () {
allows(this.title, 5);
allows(this.title, 0);
Expand Down
48 changes: 46 additions & 2 deletions test/request/jwt_request.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,26 @@ describe('request parameter features', () => {
});

describe('configuration features.requestUri', () => {
it('extends discovery', function () {
return this.agent.get('/.well-known/openid-configuration')
it('extends discovery', async function () {
await this.agent.get('/.well-known/openid-configuration')
.expect(200)
.expect((response) => {
expect(response.body).to.have.property('request_parameter_supported', true);
expect(response.body).not.to.have.property('require_signed_request_object');
});

i(this.provider).configuration('features.requestObjects').requireSignedRequestObject = true;

await this.agent.get('/.well-known/openid-configuration')
.expect(200)
.expect((response) => {
expect(response.body).to.have.property('request_parameter_supported', true);
expect(response.body).to.have.property('require_signed_request_object', true);
});
});

after(function () {
i(this.provider).configuration('features.requestObjects').requireSignedRequestObject = false;
});
});

Expand Down Expand Up @@ -243,6 +257,36 @@ describe('request parameter features', () => {
.expect(successFnCheck));
});

it('works with signed by none unless the client is required to use SIGNED request object', function () {
const spy = sinon.spy();
this.provider.once(errorEvt, spy);

return JWT.sign({
client_id: 'client-requiredSignedRequestObject',
response_type: 'code',
redirect_uri: 'https://client.example.com/cb',
}, null, 'none', { issuer: 'client-requiredSignedRequestObject', audience: this.provider.issuer }).then((request) => this.wrap({
agent: this.agent,
route,
verb,
auth: {
request,
scope: 'openid',
client_id: 'client-requiredSignedRequestObject',
response_type: 'code',
},
})
.expect(errorCode)
.expect(() => {
expect(spy.calledOnce).to.be.true;
expect(spy.args[0][1]).to.have.property('message', 'invalid_request_object');
expect(spy.args[0][1]).to.have.property(
'error_description',
'Request Object must not be unsigned for this client',
);
}));
});

describe('JAR only request', () => {
it('works without any other params', function () {
return JWT.sign({
Expand Down
7 changes: 7 additions & 0 deletions test/request/request.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ module.exports = {
client_secret: 'its48bytes_____________________________________!',
grant_types: ['urn:ietf:params:oauth:grant-type:device_code', 'authorization_code'],
redirect_uris: ['https://client.example.com/cb'],
}, {
client_id: 'client-requiredSignedRequestObject',
token_endpoint_auth_method: 'none',
require_signed_request_object: true,
client_secret: 'its48bytes_____________________________________!',
grant_types: ['urn:ietf:params:oauth:grant-type:device_code', 'authorization_code'],
redirect_uris: ['https://client.example.com/cb'],
}, {
client_id: 'client-with-HS-sig',
token_endpoint_auth_method: 'none',
Expand Down
2 changes: 2 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export interface AnyClientMetadata {
web_message_uris?: string[];
tls_client_certificate_bound_access_tokens?: boolean;

require_signed_request_object?: boolean;
require_pushed_authorization_requests?: boolean;

[key: string]: any;
Expand Down Expand Up @@ -926,6 +927,7 @@ export interface Configuration {
request?: boolean;
requestUri?: boolean;
requireUriRegistration?: boolean;
requireSignedRequestObject?: boolean;
mergingStrategy?: {
name?: 'lax' | 'strict' | 'whitelist',
whitelist?: string[] | Set<string>;
Expand Down

0 comments on commit cb12761

Please sign in to comment.