Skip to content

Commit

Permalink
feat: update of draft-ietf-oauth-resource-indicators from 00 to 01
Browse files Browse the repository at this point in the history
- you can now track and persist resources throughout the whole grant flow
- updated the error from invalid_resource to invalid_target
- device_authorization_endpoint now accepts the resource param

Closes #385
  • Loading branch information
panva committed Nov 17, 2018
1 parent 21b56bb commit 1302a54
Show file tree
Hide file tree
Showing 25 changed files with 546 additions and 244 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ The following drafts/experimental specifications are implemented by oidc-provide
- [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - draft 01][jarm]
- [OAuth 2.0 Device Flow for Browserless and Input Constrained Devices - draft 12][device-flow]
- [OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens - draft 12][mtls]
- [OAuth 2.0 Resource Indicators - draft 00][resource-indicators]
- [OAuth 2.0 Resource Indicators - draft 01][resource-indicators]
- [OAuth 2.0 Web Message Response Mode - draft 00][wmrm]
- [OpenID Connect Back-Channel Logout 1.0 - draft 04][backchannel-logout]
- [OpenID Connect Front-Channel Logout 1.0 - draft 02][frontchannel-logout]
Expand Down Expand Up @@ -183,7 +183,7 @@ See the list of available emitted [event names](/docs/events.md) and their descr
[suggest-feature]: https://github.com/panva/node-oidc-provider/issues/new?template=feature-request.md
[bug]: https://github.com/panva/node-oidc-provider/issues/new?template=bug-report.md
[mtls]: https://tools.ietf.org/html/draft-ietf-oauth-mtls-12
[resource-indicators]: https://tools.ietf.org/html/draft-ietf-oauth-resource-indicators-00
[resource-indicators]: https://tools.ietf.org/html/draft-ietf-oauth-resource-indicators-01
[jarm]: https://openid.net/specs/openid-financial-api-jarm-wd-01.html
[support-patreon]: https://www.patreon.com/panva
[support-paypal]: https://www.paypal.me/panva
46 changes: 29 additions & 17 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1115,9 +1115,9 @@ Configure `features.requestUri` with an object like so instead of a Boolean valu

### features.resourceIndicators

[draft-ietf-oauth-resource-indicators-00](https://tools.ietf.org/html/draft-ietf-oauth-resource-indicators-00) - Resource Indicators for OAuth 2.0
[draft-ietf-oauth-resource-indicators-01](https://tools.ietf.org/html/draft-ietf-oauth-resource-indicators-01) - Resource Indicators for OAuth 2.0

Enables the use and validations of `resource` parameter for the authorization and token endpoints. In order for the feature to be any useful you must also use the `audiences` helper function to further validate/whitelist the resource(s) and push them down to issued access tokens.
Enables the use of `resource` parameter for the authorization and token endpoints. In order for the feature to be any useful you must also use the `audiences` helper function to validate the resource(s) and transform it to jwt's token audience.



Expand All @@ -1126,35 +1126,47 @@ _**default value**_:
false
```
<details>
<summary>(Click to expand) Example use with audiences and dynamic AccessToken format</summary>
<summary>(Click to expand) Example use</summary>
<br>


This example will
- throw when multiple resources are requested (per spec at the OPs discretion)
- throw based on an OP policy
- push resources down to the audience of access tokens
- throw based on an OP policy when unrecognized or unauthorized resources are requested
- transform resources to audience and push them down to the audience of access tokens
- take both, the parameter and previously granted resources into consideration


```js
// const { InvalidResource } = Provider.errors;
// resourceAllowedForClient is the custom OP policy
// const { InvalidTarget } = Provider.errors;
// `resourceAllowedForClient` is the custom OP policy
// `transform` is mapping the resource values to actual aud values
{
// ...
async audiences(ctx, sub, token, use) {
const { resource } = ctx.oidc.params;
if (resource && use === 'access_token') {
if (Array.isArray(resource)) {
throw new InvalidResource('multiple "resource" parameters are not allowed');
async function audiences(ctx, sub, token, use) {
if (use === 'access_token') {
const { oidc: { route, client, params: { resource: resourceParam } } } = ctx;
let grantedResource;
if (route === 'token') {
const { oidc: { params: { grant_type } } } = ctx;
switch (grant_type) {
case 'authorization_code':
grantedResource = ctx.oidc.entities.AuthorizationCode.resource;
break;
case 'refresh_token':
grantedResource = ctx.oidc.entities.RefreshToken.resource;
break;
case 'urn:ietf:params:oauth:grant-type:device_code':
grantedResource = ctx.oidc.entities.DeviceCode.resource;
break;
default:
}
}
const { client } = ctx.oidc;
const allowed = await resourceAllowedForClient(resource, client.clientId);
const allowed = await resourceAllowedForClient(resourceParam, grantedResource, client);
if (!allowed) {
throw new InvalidResource('unauthorized "resource" requested');
}
return [resource];
return transform(resourceParam, grantedResource); // => array of validated and transformed string audiences
}
return undefined;
},
formats: {
default: 'opaque',
Expand Down
4 changes: 2 additions & 2 deletions lib/actions/authorization/decode_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const { InvalidRequestObject } = require('../../helpers/errors');
*
* @throws: invalid_request_object
*/
module.exports = (provider, PARAM_LIST, arrayResource) => {
module.exports = (provider, PARAM_LIST) => {
const { keystore, configuration: conf } = instance(provider);

return async function decodeRequest(ctx, next) {
Expand Down Expand Up @@ -95,7 +95,7 @@ module.exports = (provider, PARAM_LIST, arrayResource) => {
if (PARAM_LIST.has(key)) {
if (key === 'claims' && isPlainObject(value)) {
acc[key] = JSON.stringify(value);
} else if (key === 'resource' && arrayResource && Array.isArray(value)) {
} else if (key === 'resource' && Array.isArray(value) && conf('features.resourceIndicators')) {
acc[key] = value;
} else if (typeof value !== 'string') {
acc[key] = String(value);
Expand Down
8 changes: 7 additions & 1 deletion lib/actions/authorization/device_user_flow_response.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ const debug = require('debug')('oidc-provider:authentication:success');
const instance = require('../../helpers/weak_cache');

module.exports = provider => async function deviceVerificationResponse(ctx, next) {
const { deviceFlowSuccess } = instance(provider).configuration();
const {
deviceFlowSuccess, features: { resourceIndicators },
} = instance(provider).configuration();
const code = ctx.oidc.deviceCode;

Object.assign(code, {
Expand All @@ -20,6 +22,10 @@ module.exports = provider => async function deviceVerificationResponse(ctx, next
code.sid = ctx.oidc.session.sidFor(ctx.oidc.client.clientId);
}

if (resourceIndicators) {
code.resource = ctx.oidc.params.resource;
}

await code.save();

await deviceFlowSuccess(ctx);
Expand Down
10 changes: 4 additions & 6 deletions lib/actions/authorization/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const paramsMiddleware = require('../../shared/assemble_params');
const sessionMiddleware = require('../../shared/session');
const instance = require('../../helpers/weak_cache');
const { PARAM_LIST } = require('../../consts');
const getCheckResource = require('../../shared/check_resource');
const getCheckResourceFormat = require('../../shared/check_resource_format');

const checkClient = require('./check_client');
const checkResponseMode = require('./check_response_mode');
Expand Down Expand Up @@ -75,9 +75,7 @@ module.exports = function authorizationAction(provider, endpoint) {
}

let rejectDupesMiddleware = rejectDupes;
let resource = false;
if (endpoint === A && resourceIndicators) {
resource = true;
if (resourceIndicators) {
whitelist.add('resource');
rejectDupesMiddleware = rejectDupes.except.bind(undefined, new Set(['resource']));
}
Expand Down Expand Up @@ -117,14 +115,14 @@ module.exports = function authorizationAction(provider, endpoint) {
use(() => oauthRequired, A );
use(() => checkOpenidPresent, A );
use(() => fetchRequestUri(provider), A, DA );
use(() => decodeRequest(provider, whitelist, resource), A, DA );
use(() => decodeRequest(provider, whitelist), A, DA );
use(() => oidcRequired, A );
use(() => checkPrompt(provider), A, DA );
use(() => checkResponseType(provider), A );
use(() => checkScope(provider, whitelist), A, DA );
use(() => checkRedirectUri, A );
use(() => checkWebMessageUri(provider), A );
use(() => getCheckResource(provider), A );
use(() => getCheckResourceFormat(provider), A, DA );
use(() => checkPixy(provider), A, DA );
use(() => assignDefaults, A, DA );
use(() => checkClaims(provider), A, DA );
Expand Down
6 changes: 5 additions & 1 deletion lib/actions/authorization/process_response_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module.exports = (provider) => {
const { IdToken, AccessToken, AuthorizationCode } = provider;

const {
features: { pkce, conformIdTokenClaims },
features: { pkce, conformIdTokenClaims, resourceIndicators },
audiences,
} = instance(provider).configuration();

Expand Down Expand Up @@ -58,6 +58,10 @@ module.exports = (provider) => {

ctx.oidc.entity('AuthorizationCode', ac);

if (resourceIndicators) {
ac.resource = ctx.oidc.params.resource;
}

return { code: await ac.save() };
}

Expand Down
1 change: 1 addition & 0 deletions lib/actions/grants/authorization_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ module.exports.handler = function getAuthorizationCodeHandler(provider) {
grantId: code.grantId,
nonce: code.nonce,
scope: code.scope,
resource: code.resource,
sid: code.sid,
});

Expand Down
1 change: 1 addition & 0 deletions lib/actions/grants/refresh_token.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ module.exports.handler = function getRefreshTokenHandler(provider) {
grantId: refreshToken.grantId,
nonce: refreshToken.nonce,
sid: refreshToken.sid,
resource: refreshToken.resource,
gty: refreshToken.gty,
});

Expand Down
4 changes: 2 additions & 2 deletions lib/actions/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const getTokenAuth = require('../shared/token_auth');
const bodyParser = require('../shared/selective_body');
const rejectDupes = require('../shared/reject_dupes');
const getParams = require('../shared/assemble_params');
const getCheckResource = require('../shared/check_resource');
const getCheckResourceFormat = require('../shared/check_resource_format');

const grantTypeSet = new Set(['grant_type']);

Expand Down Expand Up @@ -41,7 +41,7 @@ module.exports = function tokenAction(provider) {
await next();
},

getCheckResource(provider),
getCheckResourceFormat(provider),

async function supportedGrantTypeCheck(ctx, next) {
presence(ctx, 'grant_type');
Expand Down
52 changes: 31 additions & 21 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,42 +489,52 @@ const DEFAULTS = {
/*
* features.resourceIndicators
*
* title: [draft-ietf-oauth-resource-indicators-00](https://tools.ietf.org/html/draft-ietf-oauth-resource-indicators-00) - Resource Indicators for OAuth 2.0
* title: [draft-ietf-oauth-resource-indicators-01](https://tools.ietf.org/html/draft-ietf-oauth-resource-indicators-01) - Resource Indicators for OAuth 2.0
*
* description: Enables the use and validations of `resource` parameter for the authorization
* and token endpoints. In order for the feature to be any useful you must also use the
* `audiences` helper function to further validate/whitelist the resource(s) and push them
* down to issued access tokens.
* description: Enables the use of `resource` parameter for the authorization and token
* endpoints. In order for the feature to be any useful you must also use the `audiences`
* helper function to validate the resource(s) and transform it to jwt's token audience.
*
* example: Example use with audiences and dynamic AccessToken format
* example: Example use
* This example will
* - throw when multiple resources are requested (per spec at the OPs discretion)
* - throw based on an OP policy
* - push resources down to the audience of access tokens
* - throw based on an OP policy when unrecognized or unauthorized resources are requested
* - transform resources to audience and push them down to the audience of access tokens
* - take both, the parameter and previously granted resources into consideration
*
* ```js
* // const { InvalidResource } = Provider.errors;
* // resourceAllowedForClient is the custom OP policy
* // const { InvalidTarget } = Provider.errors;
* // `resourceAllowedForClient` is the custom OP policy
* // `transform` is mapping the resource values to actual aud values
*
* {
* // ...
* async audiences(ctx, sub, token, use) {
* const { resource } = ctx.oidc.params;
* if (resource && use === 'access_token') {
* if (Array.isArray(resource)) {
* throw new InvalidResource('multiple "resource" parameters are not allowed');
* async function audiences(ctx, sub, token, use) {
* if (use === 'access_token') {
* const { oidc: { route, client, params: { resource: resourceParam } } } = ctx;
* let grantedResource;
* if (route === 'token') {
* const { oidc: { params: { grant_type } } } = ctx;
* switch (grant_type) {
* case 'authorization_code':
* grantedResource = ctx.oidc.entities.AuthorizationCode.resource;
* break;
* case 'refresh_token':
* grantedResource = ctx.oidc.entities.RefreshToken.resource;
* break;
* case 'urn:ietf:params:oauth:grant-type:device_code':
* grantedResource = ctx.oidc.entities.DeviceCode.resource;
* break;
* default:
* }
* }
*
* const { client } = ctx.oidc;
* const allowed = await resourceAllowedForClient(resource, client.clientId);
* const allowed = await resourceAllowedForClient(resourceParam, grantedResource, client);
* if (!allowed) {
* throw new InvalidResource('unauthorized "resource" requested');
* }
*
* return [resource];
* return transform(resourceParam, grantedResource); // => array of validated and transformed string audiences
* }
*
* return undefined;
* },
* formats: {
* default: 'opaque',
Expand Down
2 changes: 1 addition & 1 deletion lib/helpers/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const classes = [
['interaction_required'],
['invalid_request_object'],
['invalid_request_uri'],
['invalid_resource'],
['invalid_target'],
['login_required'],
['redirect_uri_mismatch', 'redirect_uri did not match any client\'s registered redirect_uris'],
['registration_not_supported', 'registration parameter provided but not supported'],
Expand Down
8 changes: 4 additions & 4 deletions lib/models/authorization_code.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const storesPKCE = require('./mixins/stores_pkce');
const storesAuth = require('./mixins/stores_auth');
const hasFormat = require('./mixins/has_format');
const consumable = require('./mixins/consumable');
const apply = require('./mixins/apply');
const consumable = require('./mixins/consumable');
const hasFormat = require('./mixins/has_format');
const storesAuth = require('./mixins/stores_auth');
const storesPKCE = require('./mixins/stores_pkce');

module.exports = provider => class AuthorizationCode extends apply([
consumable(provider),
Expand Down
8 changes: 4 additions & 4 deletions lib/models/device_code.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
const assert = require('assert');

const storesAuth = require('./mixins/stores_auth');
const hasFormat = require('./mixins/has_format');
const apply = require('./mixins/apply');
const consumable = require('./mixins/consumable');
const storesPKCE = require('./mixins/stores_pkce');
const hasFormat = require('./mixins/has_format');
const hasGrantType = require('./mixins/has_grant_type');
const apply = require('./mixins/apply');
const storesAuth = require('./mixins/stores_auth');
const storesPKCE = require('./mixins/stores_pkce');

module.exports = provider => class DeviceCode extends apply([
storesPKCE,
Expand Down
1 change: 1 addition & 0 deletions lib/models/mixins/stores_auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = superclass => class extends superclass {
'claims',
'grantId',
'nonce',
'resource',
'scope',
'sid',
];
Expand Down
6 changes: 3 additions & 3 deletions lib/models/refresh_token.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const storesAuth = require('./mixins/stores_auth');
const hasFormat = require('./mixins/has_format');
const apply = require('./mixins/apply');
const consumable = require('./mixins/consumable');
const hasFormat = require('./mixins/has_format');
const hasGrantType = require('./mixins/has_grant_type');
const apply = require('./mixins/apply');
const storesAuth = require('./mixins/stores_auth');

module.exports = provider => class RefreshToken extends apply([
consumable(provider),
Expand Down
38 changes: 0 additions & 38 deletions lib/shared/check_resource.js

This file was deleted.

Loading

0 comments on commit 1302a54

Please sign in to comment.