Skip to content

Commit

Permalink
feat: experimental support for pushed request objects
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Aug 30, 2019
1 parent 6133d43 commit 4ac3905
Show file tree
Hide file tree
Showing 30 changed files with 578 additions and 115 deletions.
3 changes: 2 additions & 1 deletion certification/fapi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const fapi = new Server(ISSUER, {
introspection: { enabled: true },
jwtIntrospection: { enabled: true },
jwtResponseModes: { enabled: true },
pushedRequestObjects: { enabled: true },
request: { enabled: true },
requestUri: { enabled: true, requireUriRegistration: true },
revocation: { enabled: true },
Expand Down Expand Up @@ -177,7 +178,7 @@ if (process.env.NODE_ENV === 'production') {

switch (ctx.oidc && ctx.oidc.route) {
case 'discovery': {
['token', 'introspection', 'revocation', 'userinfo'].forEach((endpoint) => {
['token', 'introspection', 'revocation', 'userinfo', 'request_object'].forEach((endpoint) => {
if (ctx.body[`${endpoint}_endpoint`].startsWith(ISSUER)) {
ctx.body[`${endpoint}_endpoint`] = ctx.body[`${endpoint}_endpoint`].replace('https://', 'https://mtls.');
}
Expand Down
2 changes: 1 addition & 1 deletion certification/oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ let server;
switch (ctx.oidc && ctx.oidc.route) {
case 'discovery': {
ctx.body.mtls_endpoint_aliases = {};
['token', 'introspection', 'revocation', 'userinfo', 'device_authorization'].forEach((endpoint) => {
['token', 'introspection', 'revocation', 'userinfo', 'device_authorization', 'request_object'].forEach((endpoint) => {
if (!ctx.body[`${endpoint}_endpoint`]) {
return;
}
Expand Down
17 changes: 17 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ If you or your business use oidc-provider, please consider becoming a [Patron][s
- [jwtResponseModes](#featuresjwtresponsemodes)
- [jwtUserinfo](#featuresjwtuserinfo)
- [mTLS](#featuresmtls)
- [pushedRequestObjects](#featurespushedrequestobjects)
- [registration](#featuresregistration)
- [registrationManagement](#featuresregistrationmanagement)
- [requestObjects](#featuresrequestobjects)
Expand Down Expand Up @@ -1220,6 +1221,21 @@ false

</details>

### features.pushedRequestObjects

[openid-financial-api-pushed-request-object-37426f5](https://bitbucket.org/openid/fapi/src/37426f5/Financial_API_Pushed_Request_Object.md) - Pushed Request Object

Enables the use `request_object_endpoint` defined by the Pushed Request Object draft.



_**default value**_:
```js
{
enabled: false
}
```

### features.registration

[Dynamic Client Registration 1.0](https://openid.net/specs/openid-connect-registration-1_0.html)
Expand Down Expand Up @@ -2902,6 +2918,7 @@ _**default value**_:
introspection: '/token/introspection',
jwks: '/jwks',
registration: '/reg',
request_object: '/request',
revocation: '/token/revocation',
token: '/token',
userinfo: '/me'
Expand Down
6 changes: 6 additions & 0 deletions docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ loaded client or session.
| `interaction.saved` | `(interaction)` | ... whenever interaction session is saved |
| `interaction.started` | `(ctx, interaction)` | ... whenever interaction is being requested from the end-user |
| `introspection.error` | `(ctx, error)` | ... whenever a handled error is encountered in the `introspection` endpoint |
| `replay_detection.destroyed` | `(token)` | ... whenever a replay detection object is destroyed |
| `replay_detection.saved` | `(token)` | ... whenever a replay detection object is saved |
| `request_object.error` | `(ctx, error)` | ... whenever a handled error is encountered in the POST `request_object` endpoint |
| `request_object.success` | `(ctx, client)` | ... with every successful request object endpoint response |
| `request_object.destroyed` | `(token)` | ... whenever a pushed request object is destroyed |
| `request_object.saved` | `(token)` | ... whenever a pushed request object is saved |
| `refresh_token.consumed` | `(token)` | ... whenever a refresh token is consumed |
| `refresh_token.destroyed` | `(token)` | ... whenever a refresh token is destroyed |
| `refresh_token.saved` | `(token)` | ... whenever a refresh token is saved |
Expand Down
3 changes: 2 additions & 1 deletion example/my_adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class MyAdapter {
* @constructor
* @param {string} name Name of the oidc-provider model. One of "Session", "AccessToken",
* "AuthorizationCode", "RefreshToken", "ClientCredentials", "Client", "InitialAccessToken",
* "RegistrationAccessToken", "DeviceCode", "Interaction" or "ReplayDetection"
* "RegistrationAccessToken", "DeviceCode", "Interaction", "ReplayDetection", or "RequestObject"
*
*/
constructor(name) {
Expand Down Expand Up @@ -85,6 +85,7 @@ class MyAdapter {
* - errorDescription {string} - [DeviceCode only] - error_description from authnz to be returned
* to the polling client
* - policies {string[]} - [InitialAccessToken, RegistrationAccessToken only] array of policies
* - request {string} - [RequestObject only] Pushed Request Object value
*
*
* when `jwt`
Expand Down
26 changes: 23 additions & 3 deletions lib/actions/authorization/check_client.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
const { strict: assert } = require('assert');

const { InvalidClient, InvalidRequestObject } = require('../../helpers/errors');
const {
InvalidClient, InvalidRequestObject,
} = require('../../helpers/errors');
const presence = require('../../helpers/validate_presence');
const base64url = require('../../helpers/base64url');
const instance = require('../../helpers/weak_cache');
const { PUSHED_REQUEST_URN } = require('../../consts');

const rejectRequestAndUri = require('./reject_request_and_uri');
const loadPushedRequestObject = require('./load_pushed_request_object');

/*
* Checks client_id
Expand All @@ -14,15 +21,28 @@ const base64url = require('../../helpers/base64url');
*/
module.exports = async function checkClient(ctx, next) {
const { oidc: { params } } = ctx;
const { pushedRequestObjects } = instance(ctx.oidc.provider).configuration('features');

try {
presence(ctx, 'client_id');
} catch (err) {
const { request } = params;
if (request === undefined) {
const { request_uri: requestUri } = params;
let { request } = params;

if (
!(pushedRequestObjects.enabled && requestUri && requestUri.startsWith(PUSHED_REQUEST_URN))
&& request === undefined
) {
throw err;
}

rejectRequestAndUri(ctx, () => {});

if (requestUri) {
const loadedRequestObject = await loadPushedRequestObject(ctx);
request = loadedRequestObject.request;
}

const parts = request.split('.');
let decoded;
let clientId;
Expand Down
32 changes: 23 additions & 9 deletions lib/actions/authorization/fetch_request_uri.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
const { URL } = require('url');
const { strict: assert } = require('assert');

const { InvalidRequest, InvalidRequestUri } = require('../../helpers/errors');
const { InvalidRequestUri } = require('../../helpers/errors');
const instance = require('../../helpers/weak_cache');
const { PUSHED_REQUEST_URN } = require('../../consts');

const allowedSchemes = new Set(['http:', 'https:', 'urn:']);

const loadPushedRequestObject = require('./load_pushed_request_object');
const rejectRequestAndUri = require('./reject_request_and_uri');

/*
* Validates request_uri length, protocol and its presence in client whitelist and either uses
* previously cached response or loads a fresh state. Removes request_uri form the parameters and
Expand All @@ -19,11 +23,10 @@ const allowedSchemes = new Set(['http:', 'https:', 'urn:']);
* @throws: request_uri_not_supported
*/
module.exports = async function fetchRequestUri(ctx, next) {
const { pushedRequestObjects } = instance(ctx.oidc.provider).configuration('features');
const { params } = ctx.oidc;

if (params.request !== undefined && params.request_uri !== undefined) {
throw new InvalidRequest('request and request_uri parameters MUST NOT be used together');
}
rejectRequestAndUri(ctx, () => {});

if (params.request_uri !== undefined) {
let protocol;
Expand All @@ -34,7 +37,14 @@ module.exports = async function fetchRequestUri(ctx, next) {
throw new InvalidRequestUri('invalid request_uri scheme');
}

if (ctx.oidc.client.requestUris || protocol === 'urn:') {
let loadedRequestObject = ctx.oidc.entities.RequestObject;
if (
!loadedRequestObject
&& pushedRequestObjects.enabled
&& params.request_uri.startsWith(PUSHED_REQUEST_URN)
) {
loadedRequestObject = await loadPushedRequestObject(ctx);
} else if (!loadedRequestObject && (ctx.oidc.client.requestUris || protocol === 'urn:')) {
if (!ctx.oidc.client.requestUriAllowed(params.request_uri)) {
throw new InvalidRequestUri('provided request_uri is not whitelisted');
}
Expand All @@ -44,13 +54,17 @@ module.exports = async function fetchRequestUri(ctx, next) {
ctx.oidc.insecureRequestUri = true;
}

const cache = instance(ctx.oidc.provider).requestUriCache;

try {
if (protocol === 'urn:') {
params.request = await cache.resolveUrn(params.request_uri);
if (loadedRequestObject) {
params.request = loadedRequestObject.request;
} else {
params.request = await cache.resolveWebUri(params.request_uri);
const cache = instance(ctx.oidc.provider).requestUriCache;
if (protocol === 'urn:') {
params.request = await cache.resolveUrn(params.request_uri);
} else {
params.request = await cache.resolveWebUri(params.request_uri);
}
}
assert(params.request);
params.request_uri = undefined;
Expand Down
102 changes: 54 additions & 48 deletions lib/actions/authorization/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,20 @@ const deviceAuthorizationResponse = require('./device_authorization_response');
const deviceAuthorizationClientId = require('./device_authorization_client_id');
const deviceUserFlow = require('./device_user_flow');
const deviceUserFlowResponse = require('./device_user_flow_response');
const requestObjectRemapErrors = require('./request_object_remap_errors');
const requestObjectEndpointParameters = require('./request_object_endpoint_params');
const requestObjectResponse = require('./request_object_response');


const _ = undefined;
const A = 'authorization';
const R = 'resume';
const DA = 'device_authorization';
const CV = 'code_verification';
const DR = 'device_resume';
const ALL = [A, DA, R, CV, DR];
const RO = 'request_object';

const authRequired = new Set([DA, RO]);

const parseBody = bodyParser.bind(_, 'application/x-www-form-urlencoded');
const parseBody = bodyParser.bind(undefined, 'application/x-www-form-urlencoded');

module.exports = function authorizationAction(provider, endpoint) {
const {
Expand All @@ -73,10 +76,10 @@ module.exports = function authorizationAction(provider, endpoint) {
whitelist.add('claims');
}

let rejectDupesMiddleware = rejectDupes.bind(_, {});
let rejectDupesMiddleware = rejectDupes.bind(undefined, {});
if (resourceIndicators.enabled) {
whitelist.add('resource');
rejectDupesMiddleware = rejectDupes.bind(_, { except: new Set(['resource']) });
rejectDupesMiddleware = rejectDupes.bind(undefined, { except: new Set(['resource']) });
}

extraParams.forEach(Set.prototype.add.bind(whitelist));
Expand All @@ -101,53 +104,56 @@ module.exports = function authorizationAction(provider, endpoint) {
const returnTo = /^(code|device)_/.test(endpoint) ? 'device_resume' : 'resume';

/* eslint-disable no-multi-spaces, space-in-parens */
use(() => noCache, ...ALL);
use(() => sessionMiddleware, A, R, DR);
use(() => deviceUserFlow.bind(_, whitelist), CV, DR);
use(() => getResume.bind(_, whitelist, returnTo), R, DR);
use(() => parseBody, A, DA );
if (endpoint === DA) {
use(() => noCache, A, DA, R, CV, DR, RO);
use(() => sessionMiddleware, A, R, DR );
use(() => deviceUserFlow.bind(undefined, whitelist), CV, DR );
use(() => getResume.bind(undefined, whitelist, returnTo), R, DR );
use(() => parseBody, A, DA, RO);
if (authRequired.has(endpoint)) {
const { params: authParams, middleware: tokenAuth } = getTokenAuth(provider, 'token', endpoint);
use(() => paramsMiddleware.bind(_, authParams), DA );
use(() => paramsMiddleware.bind(undefined, authParams), DA, RO);
tokenAuth.forEach((tokenAuthMiddleware) => {
use(() => tokenAuthMiddleware, DA );
use(() => tokenAuthMiddleware, DA, RO);
});
}
use(() => deviceAuthorizationClientId, DA );
use(() => paramsMiddleware.bind(_, whitelist), A, DA );
use(() => rejectDupesMiddleware, A, DA );
use(() => rejectUnsupported, A, DA );
use(() => checkClient, ...ALL);
use(() => checkClientGrantType, DA );
use(() => checkResponseMode, A );
use(() => fetchRequestUri, A, DA );
use(() => deviceAuthorizationClientId, DA );
use(() => paramsMiddleware.bind(undefined, whitelist), A, DA, RO);
use(() => requestObjectEndpointParameters, RO);
use(() => rejectDupesMiddleware, A, DA, RO);
use(() => rejectUnsupported, A, DA );
use(() => checkClient, A, DA, R, CV, DR );
use(() => checkClientGrantType, DA );
use(() => checkResponseMode, A );
use(() => fetchRequestUri, A, DA );
use(() => requestObjectRemapErrors, RO);
use(() => processRequestObject.bind(
_, whitelist, rejectDupesMiddleware,
), A, DA );
use(() => oneRedirectUriClients, A );
use(() => oauthRequired, A );
use(() => rejectRegistration, A, DA );
use(() => oidcRequired, A );
use(() => assignDefaults, A, DA );
use(() => checkOpenidScope.bind(_, whitelist), A, DA );
use(() => checkPrompt, A, DA );
use(() => checkResponseType, A );
use(() => checkScope.bind(_, whitelist), A, DA );
use(() => checkRedirectUri, A );
use(() => checkWebMessageUri, A );
use(() => checkResource, A, DA );
use(() => checkPKCE, A, DA );
use(() => checkClaims, A, DA );
use(() => checkMaxAge, A, DA );
use(() => checkIdTokenHint, A, DA );
use(() => authorizationEmit, A, R, CV, DR);
use(() => assignClaims, A, R, CV, DR);
use(() => loadAccount, A, R, CV, DR);
use(() => interactions.bind(_, returnTo), A, R, CV, DR);
use(() => respond, A, R );
use(() => processResponseTypes, A, R );
use(() => deviceAuthorizationResponse, DA );
use(() => deviceUserFlowResponse, CV, DR);
undefined, whitelist, rejectDupesMiddleware,
), A, DA, RO);
use(() => oneRedirectUriClients, A, RO);
use(() => oauthRequired, A, RO);
use(() => rejectRegistration, A, DA, RO);
use(() => oidcRequired, A, RO);
use(() => assignDefaults, A, DA );
use(() => checkOpenidScope.bind(undefined, whitelist), A, DA, RO);
use(() => checkPrompt, A, DA, RO);
use(() => checkResponseType, A, RO);
use(() => checkScope.bind(undefined, whitelist), A, DA, RO);
use(() => checkRedirectUri, A, RO);
use(() => checkWebMessageUri, A, RO);
use(() => checkResource, A, DA, RO);
use(() => checkPKCE, A, DA, RO);
use(() => checkClaims, A, DA, RO);
use(() => checkMaxAge, A, DA, RO);
use(() => checkIdTokenHint, A, DA, RO);
use(() => authorizationEmit, A, R, CV, DR );
use(() => assignClaims, A, R, CV, DR );
use(() => loadAccount, A, R, CV, DR );
use(() => interactions.bind(undefined, returnTo), A, R, CV, DR );
use(() => respond, A, R );
use(() => processResponseTypes, A, R );
use(() => deviceAuthorizationResponse, DA );
use(() => deviceUserFlowResponse, CV, DR );
use(() => requestObjectResponse, RO);
/* eslint-enable no-multi-spaces, space-in-parens */

return stack;
Expand Down
13 changes: 13 additions & 0 deletions lib/actions/authorization/load_pushed_request_object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const { PUSHED_REQUEST_URN } = require('../../consts');
const { InvalidRequestUri } = require('../../helpers/errors');

module.exports = async function loadPushedRequestObject(ctx) {
const { params } = ctx.oidc;
const [, id] = params.request_uri.split(PUSHED_REQUEST_URN);
const requestObject = await ctx.oidc.provider.RequestObject.find(id);
if (!requestObject) {
throw new InvalidRequestUri('request_uri is invalid or expired');
}
ctx.oidc.entity('RequestObject', requestObject);
return requestObject;
};
2 changes: 1 addition & 1 deletion lib/actions/authorization/oauth_required.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const presence = require('../../helpers/validate_presence');
*/
module.exports = function oauthRequired(ctx, next) {
// Validate: required oauth params
presence(ctx, 'response_type');
presence(ctx, 'response_type', 'client_id');

return next();
};
Loading

0 comments on commit 4ac3905

Please sign in to comment.