From 1302a54c96a41ade2c399eaf7004394cdf410e9c Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 13 Nov 2018 16:45:49 +0100 Subject: [PATCH] feat: update of draft-ietf-oauth-resource-indicators from 00 to 01 - 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 --- README.md | 4 +- docs/configuration.md | 46 +- lib/actions/authorization/decode_request.js | 4 +- .../device_user_flow_response.js | 8 +- lib/actions/authorization/index.js | 10 +- .../authorization/process_response_types.js | 6 +- lib/actions/grants/authorization_code.js | 1 + lib/actions/grants/refresh_token.js | 1 + lib/actions/token.js | 4 +- lib/helpers/defaults.js | 52 +- lib/helpers/errors.js | 2 +- lib/models/authorization_code.js | 8 +- lib/models/device_code.js | 8 +- lib/models/mixins/stores_auth.js | 1 + lib/models/refresh_token.js | 6 +- lib/shared/check_resource.js | 38 -- lib/shared/check_resource_format.js | 34 ++ .../code_verification_endpoint.test.js | 2 + test/device_code/device_code.config.js | 1 + test/provider/provider_class.test.js | 2 +- .../resource_indicators.config.js | 49 +- .../resource_indicators.test.js | 485 +++++++++++++----- test/storage/jwt.test.js | 6 +- test/storage/legacy.test.js | 6 +- test/storage/opaque.test.js | 6 +- 25 files changed, 546 insertions(+), 244 deletions(-) delete mode 100644 lib/shared/check_resource.js create mode 100644 lib/shared/check_resource_format.js diff --git a/README.md b/README.md index 8f4d6bb47..250971296 100644 --- a/README.md +++ b/README.md @@ -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] @@ -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 diff --git a/docs/configuration.md b/docs/configuration.md index 39d853b94..e3fffba72 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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. @@ -1126,35 +1126,47 @@ _**default value**_: false ```
- (Click to expand) Example use with audiences and dynamic AccessToken format + (Click to expand) 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', diff --git a/lib/actions/authorization/decode_request.js b/lib/actions/authorization/decode_request.js index faaf543b8..36e027b1b 100644 --- a/lib/actions/authorization/decode_request.js +++ b/lib/actions/authorization/decode_request.js @@ -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) { @@ -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); diff --git a/lib/actions/authorization/device_user_flow_response.js b/lib/actions/authorization/device_user_flow_response.js index 6ffe9f0b4..16ce0953e 100644 --- a/lib/actions/authorization/device_user_flow_response.js +++ b/lib/actions/authorization/device_user_flow_response.js @@ -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, { @@ -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); diff --git a/lib/actions/authorization/index.js b/lib/actions/authorization/index.js index 0beff9dae..364d9dd27 100644 --- a/lib/actions/authorization/index.js +++ b/lib/actions/authorization/index.js @@ -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'); @@ -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'])); } @@ -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 ); diff --git a/lib/actions/authorization/process_response_types.js b/lib/actions/authorization/process_response_types.js index e9e94266f..71869fcb0 100644 --- a/lib/actions/authorization/process_response_types.js +++ b/lib/actions/authorization/process_response_types.js @@ -6,7 +6,7 @@ module.exports = (provider) => { const { IdToken, AccessToken, AuthorizationCode } = provider; const { - features: { pkce, conformIdTokenClaims }, + features: { pkce, conformIdTokenClaims, resourceIndicators }, audiences, } = instance(provider).configuration(); @@ -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() }; } diff --git a/lib/actions/grants/authorization_code.js b/lib/actions/grants/authorization_code.js index 6d2ed6f12..5abc6e893 100644 --- a/lib/actions/grants/authorization_code.js +++ b/lib/actions/grants/authorization_code.js @@ -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, }); diff --git a/lib/actions/grants/refresh_token.js b/lib/actions/grants/refresh_token.js index 1d1eb0c6b..a8a24947a 100644 --- a/lib/actions/grants/refresh_token.js +++ b/lib/actions/grants/refresh_token.js @@ -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, }); diff --git a/lib/actions/token.js b/lib/actions/token.js index ddf969052..b03464c8d 100644 --- a/lib/actions/token.js +++ b/lib/actions/token.js @@ -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']); @@ -41,7 +41,7 @@ module.exports = function tokenAction(provider) { await next(); }, - getCheckResource(provider), + getCheckResourceFormat(provider), async function supportedGrantTypeCheck(ctx, next) { presence(ctx, 'grant_type'); diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index 8e0673993..319a57922 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -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', diff --git a/lib/helpers/errors.js b/lib/helpers/errors.js index 6d494ad41..2e731b954 100644 --- a/lib/helpers/errors.js +++ b/lib/helpers/errors.js @@ -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'], diff --git a/lib/models/authorization_code.js b/lib/models/authorization_code.js index c2e3671f2..e38642784 100644 --- a/lib/models/authorization_code.js +++ b/lib/models/authorization_code.js @@ -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), diff --git a/lib/models/device_code.js b/lib/models/device_code.js index eb003b607..5f66bac4f 100644 --- a/lib/models/device_code.js +++ b/lib/models/device_code.js @@ -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, diff --git a/lib/models/mixins/stores_auth.js b/lib/models/mixins/stores_auth.js index 48e923f40..df20647a1 100644 --- a/lib/models/mixins/stores_auth.js +++ b/lib/models/mixins/stores_auth.js @@ -9,6 +9,7 @@ module.exports = superclass => class extends superclass { 'claims', 'grantId', 'nonce', + 'resource', 'scope', 'sid', ]; diff --git a/lib/models/refresh_token.js b/lib/models/refresh_token.js index 0b038626e..498875c9b 100644 --- a/lib/models/refresh_token.js +++ b/lib/models/refresh_token.js @@ -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), diff --git a/lib/shared/check_resource.js b/lib/shared/check_resource.js deleted file mode 100644 index ce1cdc15d..000000000 --- a/lib/shared/check_resource.js +++ /dev/null @@ -1,38 +0,0 @@ -const { URL } = require('url'); - -const instance = require('../helpers/weak_cache'); -const { InvalidResource } = require('../helpers/errors'); - -module.exports = function getCheckResource(provider) { - return function checkResource({ oidc: { params } }, next) { - if (!instance(provider).configuration('features.resourceIndicators') || params.resource === undefined) { - return next(); - } - - let requested = params.resource; - if (!Array.isArray(requested)) { - requested = [requested]; - } - - requested.forEach((resource) => { - let href; - try { - ({ href } = new URL(resource)); // eslint-disable-line no-new - } catch (err) { - throw new InvalidResource('resource must be an absolute URI'); - } - - // NOTE: we don't check for new URL() => search of hash because of an edge case - // new URL('https://example.com?#') => they're empty, seems like an inconsistent validation - if (href.includes('#')) { - throw new InvalidResource('resource must not contain a fragment component'); - } - - if (href.includes('?')) { - throw new InvalidResource('resource must not contain a query component'); - } - }); - - return next(); - }; -}; diff --git a/lib/shared/check_resource_format.js b/lib/shared/check_resource_format.js new file mode 100644 index 000000000..b6f02a803 --- /dev/null +++ b/lib/shared/check_resource_format.js @@ -0,0 +1,34 @@ +const { URL } = require('url'); + +const instance = require('../helpers/weak_cache'); +const { InvalidTarget } = require('../helpers/errors'); + +module.exports = function getCheckResourceFormat(provider) { + return function checkResourceFormat({ oidc: { params } }, next) { + if (!instance(provider).configuration('features.resourceIndicators') || params.resource === undefined) { + return next(); + } + + let requested = params.resource; + if (!Array.isArray(requested)) { + requested = [requested]; + } + + requested.forEach((resource) => { + let href; + try { + ({ href } = new URL(resource)); + } catch (err) { + throw new InvalidTarget('resource must be an absolute URI'); + } + + // NOTE: we don't check for new URL() => search of hash because of an edge case + // new URL('https://example.com?#') => search and hash are empty, seems like an inconsistent validation + if (href.includes('#')) { + throw new InvalidTarget('resource must not contain a fragment component'); + } + }); + + return next(); + }; +}; diff --git a/test/device_code/code_verification_endpoint.test.js b/test/device_code/code_verification_endpoint.test.js index a22e52832..a5b8da704 100644 --- a/test/device_code/code_verification_endpoint.test.js +++ b/test/device_code/code_verification_endpoint.test.js @@ -321,6 +321,7 @@ describe('POST code_verification endpoint w/ verification', () => { scope: 'openid email', client_id: 'client', claims: JSON.stringify({ userinfo: { email: null } }), + resource: 'urn:foo:bar', }, }).save(); @@ -341,6 +342,7 @@ describe('POST code_verification endpoint w/ verification', () => { expect(code).to.have.property('authTime', session.loginTs); expect(code).to.have.property('scope', 'openid email'); expect(code).to.have.property('claims').that.eqls({ userinfo: { email: null }, rejected: ['email_verified'] }); + expect(code).to.have.property('resource', 'urn:foo:bar'); expect(spy.calledOnce).to.be.true; }); diff --git a/test/device_code/device_code.config.js b/test/device_code/device_code.config.js index bdf13c690..be657e649 100644 --- a/test/device_code/device_code.config.js +++ b/test/device_code/device_code.config.js @@ -7,6 +7,7 @@ config.features = { request: false, claimsParameter: true, requestUri: false, + resourceIndicators: true, }; config.extraParams = [ diff --git a/test/provider/provider_class.test.js b/test/provider/provider_class.test.js index da0289545..fa9cb76a6 100644 --- a/test/provider/provider_class.test.js +++ b/test/provider/provider_class.test.js @@ -22,7 +22,7 @@ describe('Provider', () => { 'InvalidRequest', 'InvalidRequestObject', 'InvalidRequestUri', - 'InvalidResource', + 'InvalidTarget', 'InvalidScope', 'InvalidToken', 'LoginRequired', diff --git a/test/resource_indicators/resource_indicators.config.js b/test/resource_indicators/resource_indicators.config.js index 54bbdf12b..2c1e8203f 100644 --- a/test/resource_indicators/resource_indicators.config.js +++ b/test/resource_indicators/resource_indicators.config.js @@ -3,7 +3,7 @@ const { URL } = require('url'); const { cloneDeep } = require('lodash'); const config = cloneDeep(require('../default.config')); -const { errors: { InvalidResource } } = require('../../lib'); +const { errors: { InvalidTarget } } = require('../../lib'); config.whitelistedJWA.requestObjectSigningAlgValues = ['none']; config.features = { @@ -14,21 +14,46 @@ config.features = { resourceIndicators: true, }; -config.audiences = ({ oidc: { params } }, sub, token, use) => { - const { resource } = params; - if (resource && ['access_token', 'client_credentials'].includes(use)) { - let audiences = resource; - if (!Array.isArray(resource)) { - audiences = [resource]; +config.audiences = ({ oidc: { params, route, entities } }, sub, token, use) => { + if (['access_token', 'client_credentials'].includes(use)) { + const resourceParam = params.resource; + let resources = []; + if (Array.isArray(resourceParam)) { + resources = resources.concat(resourceParam); + } else if (resourceParam) { + resources.push(resourceParam); } - audiences.forEach((aud) => { + + if (route === 'token') { + const { grant_type } = params; + let grantedResource; + switch (grant_type) { + case 'authorization_code': + grantedResource = entities.AuthorizationCode.resource; + break; + case 'refresh_token': + grantedResource = entities.RefreshToken.resource; + break; + case 'urn:ietf:params:oauth:grant-type:device_code': + grantedResource = entities.DeviceCode.resource; + break; + default: + } + if (Array.isArray(grantedResource)) { + resources = resources.concat(grantedResource); + } else if (grantedResource) { + resources.push(grantedResource); + } + } + + resources.forEach((aud) => { const { protocol } = new URL(aud); - if (protocol !== 'https:') { - throw new InvalidResource('resources must be https URIs'); + if (!['https:', 'urn:'].includes(protocol)) { + throw new InvalidTarget('resources must be https URIs or URNs'); } }); - return audiences; + return resources; } return undefined; @@ -47,6 +72,6 @@ module.exports = { 'urn:ietf:params:oauth:grant-type:device_code', 'client_credentials', ], - response_types: ['code token'], + response_types: ['id_token token', 'code'], }, }; diff --git a/test/resource_indicators/resource_indicators.test.js b/test/resource_indicators/resource_indicators.test.js index 2cafc7b37..6bd85d82e 100644 --- a/test/resource_indicators/resource_indicators.test.js +++ b/test/resource_indicators/resource_indicators.test.js @@ -15,96 +15,284 @@ describe('features.resourceIndicators', () => { }); describe('urn:ietf:params:oauth:grant-type:device_code', () => { - beforeEach(async function () { - await this.agent.post('/device/auth') - .send({ - client_id: 'client', + describe('requested with device authorization request', () => { + it('allows for single resource to be requested (1/2)', async function () { + let deviceCode; + await this.agent.post('/device/auth') + .send({ + client_id: 'client', + scope: 'openid', + resource: 'https://client.example.com/api', + }) + .type('form') + .expect(200) + .expect(({ body: { device_code: dc } }) => { + deviceCode = dc; + }); + const adapter = this.TestAdapter.for('DeviceCode'); + const jti = this.getTokenJti(deviceCode); + + expect( + adapter.syncFind(jti, { payload: true }), + ).to.have.nested.property('params.resource', 'https://client.example.com/api'); + + adapter.syncUpdate(jti, { scope: 'openid', - }) - .type('form') - .expect(200) - .expect(({ body: { device_code: dc } }) => { - this.dc = dc; + accountId: 'account', + resource: 'https://client.example.com/api', }); - this.TestAdapter.for('DeviceCode').syncUpdate(this.getTokenJti(this.dc), { - scope: 'openid', - accountId: 'account', + const spy = sinon.spy(); + this.provider.once('token.issued', spy); + + await this.agent.post('/token') + .send({ + client_id: 'client', + device_code: deviceCode, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + }) + .type('form') + .expect(200); + + const [token] = spy.firstCall.args; + expect(token.aud).to.include('https://client.example.com/api'); }); - }); - it('allows for single resource to be requested', async function () { - const spy = sinon.spy(); - this.provider.once('token.issued', spy); + it('allows for single resource to be requested (2/2)', async function () { + let deviceCode; + await this.agent.post('/device/auth') + .send({ + client_id: 'client', + scope: 'openid', + resource: 'urn:foo:bar', + }) + .type('form') + .expect(200) + .expect(({ body: { device_code: dc } }) => { + deviceCode = dc; + }); + const adapter = this.TestAdapter.for('DeviceCode'); + const jti = this.getTokenJti(deviceCode); - await this.agent.post('/token') - .send({ - client_id: 'client', - device_code: this.dc, - grant_type: 'urn:ietf:params:oauth:grant-type:device_code', - resource: 'https://client.example.com/api', - }) - .type('form') - .expect(200); + expect( + adapter.syncFind(jti, { payload: true }), + ).to.have.nested.property('params.resource', 'urn:foo:bar'); - const [token] = spy.firstCall.args; - expect(token.aud).to.include('https://client.example.com/api'); - }); + adapter.syncUpdate(jti, { + scope: 'openid', + accountId: 'account', + resource: 'urn:foo:bar', + }); - it('allows for multiple resources to be requested', async function () { - const spy = sinon.spy(); - this.provider.once('token.issued', spy); + const spy = sinon.spy(); + this.provider.once('token.issued', spy); - await this.agent.post('/token') - .send(`${stringify({ - client_id: 'client', - device_code: this.dc, - grant_type: 'urn:ietf:params:oauth:grant-type:device_code', - })}&resource=${encodeURIComponent('https://client.example.com/api')}&resource=${encodeURIComponent('https://rs.example.com')}`) - .type('form') - .expect(200); + await this.agent.post('/token') + .send({ + client_id: 'client', + device_code: deviceCode, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + }) + .type('form') + .expect(200); - const [token] = spy.firstCall.args; - expect(token.aud).to.include('https://client.example.com/api'); - expect(token.aud).to.include('https://rs.example.com'); - }); + const [token] = spy.firstCall.args; + expect(token.aud).to.include('urn:foo:bar'); + }); - it('allows for arbitrary validations to be in place in the audiences helper', async function () { - await this.agent.post('/token') - .send({ - client_id: 'client', - device_code: this.dc, - grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + it('allows for multiple resources to be requested', async function () { + let deviceCode; + await this.agent.post('/device/auth') + .send(`${stringify({ + client_id: 'client', + scope: 'openid', + })}&resource=${encodeURIComponent('https://client.example.com/api')}&resource=${encodeURIComponent('https://rs.example.com')}`) + .type('form') + .expect(200) + .expect(({ body: { device_code: dc } }) => { + deviceCode = dc; + }); + const adapter = this.TestAdapter.for('DeviceCode'); + const jti = this.getTokenJti(deviceCode); + + expect( + adapter.syncFind(jti, { payload: true }), + ).to.have.deep.nested.property('params.resource', ['https://client.example.com/api', 'https://rs.example.com']); + + adapter.syncUpdate(jti, { + scope: 'openid', + accountId: 'account', + resource: ['https://client.example.com/api', 'https://rs.example.com'], + }); + + const spy = sinon.spy(); + this.provider.once('token.issued', spy); + + await this.agent.post('/token') + .send({ + client_id: 'client', + device_code: deviceCode, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + }) + .type('form') + .expect(200); + + const [token] = spy.firstCall.args; + expect(token.aud).to.include('https://client.example.com/api'); + expect(token.aud).to.include('https://rs.example.com'); + }); + + it('allows for arbitrary validations to be in place in the audiences helper', async function () { + let deviceCode; + await this.agent.post('/device/auth') + .send({ + client_id: 'client', + scope: 'openid', + resource: 'http://client.example.com/api', + }) + .type('form') + .expect(200) + .expect(({ body: { device_code: dc } }) => { + deviceCode = dc; + }); + const adapter = this.TestAdapter.for('DeviceCode'); + const jti = this.getTokenJti(deviceCode); + + expect( + adapter.syncFind(jti, { payload: true }), + ).to.have.nested.property('params.resource', 'http://client.example.com/api'); + + adapter.syncUpdate(jti, { + scope: 'openid', + accountId: 'account', resource: 'http://client.example.com/api', - }) - .type('form') - .expect(400) - .expect({ - error: 'invalid_resource', - error_description: 'resources must be https URIs', }); + + await this.agent.post('/token') + .send({ + client_id: 'client', + device_code: deviceCode, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + }) + .type('form') + .expect(400) + .expect({ + error: 'invalid_target', + error_description: 'resources must be https URIs or URNs', + }); + }); }); - it('ignores the resource parameter on device_authorization_endpoint', async function () { + describe('requested at the token endpoint', () => { + beforeEach(async function () { + await this.agent.post('/device/auth') + .send({ + client_id: 'client', + scope: 'openid', + }) + .type('form') + .expect(200) + .expect(({ body: { device_code: dc } }) => { + this.dc = dc; + }); + + this.TestAdapter.for('DeviceCode').syncUpdate(this.getTokenJti(this.dc), { + scope: 'openid', + accountId: 'account', + }); + }); + + it('allows for single resource to be requested (1/2)', async function () { + const spy = sinon.spy(); + this.provider.once('token.issued', spy); + + await this.agent.post('/token') + .send({ + client_id: 'client', + device_code: this.dc, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + resource: 'https://client.example.com/api', + }) + .type('form') + .expect(200); + + const [token] = spy.firstCall.args; + expect(token.aud).to.include('https://client.example.com/api'); + }); + + it('allows for single resource to be requested (2/2)', async function () { + const spy = sinon.spy(); + this.provider.once('token.issued', spy); + + await this.agent.post('/token') + .send({ + client_id: 'client', + device_code: this.dc, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + resource: 'urn:foo:bar', + }) + .type('form') + .expect(200); + + const [token] = spy.firstCall.args; + expect(token.aud).to.include('urn:foo:bar'); + }); + + it('allows for multiple resources to be requested', async function () { + const spy = sinon.spy(); + this.provider.once('token.issued', spy); + + await this.agent.post('/token') + .send(`${stringify({ + client_id: 'client', + device_code: this.dc, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + })}&resource=${encodeURIComponent('https://client.example.com/api')}&resource=${encodeURIComponent('https://rs.example.com')}`) + .type('form') + .expect(200); + + const [token] = spy.firstCall.args; + expect(token.aud).to.include('https://client.example.com/api'); + expect(token.aud).to.include('https://rs.example.com'); + }); + + it('allows for arbitrary validations to be in place in the audiences helper', async function () { + await this.agent.post('/token') + .send({ + client_id: 'client', + device_code: this.dc, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + resource: 'http://client.example.com/api', + }) + .type('form') + .expect(400) + .expect({ + error: 'invalid_target', + error_description: 'resources must be https URIs or URNs', + }); + }); + }); + }); + + describe('client_credentials', () => { + it('allows for single resource to be requested (1/2)', async function () { const spy = sinon.spy(); this.provider.once('token.issued', spy); - await this.agent.post('/device/auth') + await this.agent.post('/token') .send({ client_id: 'client', - scope: 'openid', + grant_type: 'client_credentials', resource: 'https://client.example.com/api', }) .type('form') .expect(200); const [token] = spy.firstCall.args; - expect(token.params).not.to.have.property('resource'); + expect(token.aud).to.include('https://client.example.com/api'); }); - }); - describe('client_credentials', () => { - it('allows for single resource to be requested', async function () { + it('allows for single resource to be requested (2/2)', async function () { const spy = sinon.spy(); this.provider.once('token.issued', spy); @@ -112,13 +300,13 @@ describe('features.resourceIndicators', () => { .send({ client_id: 'client', grant_type: 'client_credentials', - resource: 'https://client.example.com/api', + resource: 'urn:foo:bar', }) .type('form') .expect(200); const [token] = spy.firstCall.args; - expect(token.aud).to.include('https://client.example.com/api'); + expect(token.aud).to.include('urn:foo:bar'); }); it('allows for multiple resources to be requested', async function () { @@ -148,8 +336,8 @@ describe('features.resourceIndicators', () => { .type('form') .expect(400) .expect({ - error: 'invalid_resource', - error_description: 'resources must be https URIs', + error: 'invalid_target', + error_description: 'resources must be https URIs or URNs', }); }); }); @@ -158,12 +346,12 @@ describe('features.resourceIndicators', () => { before(function () { return this.login(); }); describe('authorization endpoint', () => { - it('allows for single resource to be requested', async function () { + it('allows for single resource to be requested (1/2)', async function () { const spy = sinon.spy(); this.provider.once('authorization.success', spy); const auth = new this.AuthorizationRequest({ - response_type: 'code token', + response_type: 'id_token token', scope: 'openid', resource: 'https://client.example.com/api', }); @@ -171,7 +359,7 @@ describe('features.resourceIndicators', () => { await this.wrap({ route: '/auth', verb: 'get', auth }) .expect(302) .expect(auth.validateFragment) - .expect(auth.validatePresence(['code', 'state', 'access_token', 'expires_in', 'token_type'])) + .expect(auth.validatePresence(['id_token', 'state', 'access_token', 'expires_in', 'token_type'])) .expect(auth.validateState) .expect(auth.validateClientLocation); @@ -179,12 +367,33 @@ describe('features.resourceIndicators', () => { expect(AccessToken.aud).to.include('https://client.example.com/api'); }); + it('allows for single resource to be requested (2/2)', async function () { + const spy = sinon.spy(); + this.provider.once('authorization.success', spy); + + const auth = new this.AuthorizationRequest({ + response_type: 'id_token token', + scope: 'openid', + resource: 'urn:foo:bar', + }); + + await this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(302) + .expect(auth.validateFragment) + .expect(auth.validatePresence(['id_token', 'state', 'access_token', 'expires_in', 'token_type'])) + .expect(auth.validateState) + .expect(auth.validateClientLocation); + + const [{ oidc: { entities: { AccessToken } } }] = spy.firstCall.args; + expect(AccessToken.aud).to.include('urn:foo:bar'); + }); + it('allows for multiple resources to be requested', async function () { const spy = sinon.spy(); this.provider.once('authorization.success', spy); const auth = new this.AuthorizationRequest({ - response_type: 'code token', + response_type: 'id_token token', scope: 'openid', resource: ['https://client.example.com/api', 'https://rs.example.com'], }); @@ -192,7 +401,7 @@ describe('features.resourceIndicators', () => { await this.wrap({ route: '/auth', verb: 'get', auth }) .expect(302) .expect(auth.validateFragment) - .expect(auth.validatePresence(['code', 'state', 'access_token', 'expires_in', 'token_type'])) + .expect(auth.validatePresence(['id_token', 'state', 'access_token', 'expires_in', 'token_type'])) .expect(auth.validateState) .expect(auth.validateClientLocation); @@ -206,7 +415,7 @@ describe('features.resourceIndicators', () => { this.provider.once('authorization.success', spy); const auth = new this.AuthorizationRequest({ - response_type: 'code token', + response_type: 'id_token token', scope: 'openid', resource: 'http://client.example.com/api', }); @@ -217,26 +426,26 @@ describe('features.resourceIndicators', () => { .expect(auth.validatePresence(['error', 'error_description', 'state'])) .expect(auth.validateState) .expect(auth.validateClientLocation) - .expect(auth.validateError('invalid_resource')) - .expect(auth.validateErrorDescription('resources must be https URIs')); + .expect(auth.validateError('invalid_target')) + .expect(auth.validateErrorDescription('resources must be https URIs or URNs')); }); }); describe('token endpoint', () => { - it('allows for single resource to be requested', async function () { + it('allows for single resource to be requested (1/2)', async function () { let spy = sinon.spy(); this.provider.once('grant.success', spy); const auth = new this.AuthorizationRequest({ - response_type: 'code token', + response_type: 'code', scope: 'openid', + resource: 'https://client.example.com/api', }); let code; await this.wrap({ route: '/auth', verb: 'get', auth }) .expect(302) - .expect(auth.validateFragment) - .expect(auth.validatePresence(['code', 'state', 'access_token', 'expires_in', 'token_type'])) + .expect(auth.validatePresence(['code', 'state'])) .expect(auth.validateState) .expect(auth.validateClientLocation) .expect(({ headers: { location } }) => { @@ -251,7 +460,6 @@ describe('features.resourceIndicators', () => { client_id: 'client', grant_type: 'authorization_code', redirect_uri: 'https://client.example.com/cb', - resource: 'https://client.example.com/api', }) .type('form') .expect(200) @@ -270,7 +478,6 @@ describe('features.resourceIndicators', () => { refresh_token, client_id: 'client', grant_type: 'refresh_token', - resource: 'https://client.example.com/api', }) .type('form') .expect(200) @@ -282,20 +489,77 @@ describe('features.resourceIndicators', () => { expect(AccessToken.aud).to.include('https://client.example.com/api'); }); + it('allows for single resource to be requested (2/2)', async function () { + let spy = sinon.spy(); + this.provider.once('grant.success', spy); + + const auth = new this.AuthorizationRequest({ + response_type: 'code', + scope: 'openid', + resource: 'urn:foo:bar', + }); + + let code; + await this.wrap({ route: '/auth', verb: 'get', auth }) + .expect(302) + .expect(auth.validatePresence(['code', 'state'])) + .expect(auth.validateState) + .expect(auth.validateClientLocation) + .expect(({ headers: { location } }) => { + ({ query: { code } } = url.parse(location, true)); + }); + + let AccessToken; + let refresh_token; + await this.agent.post('/token') + .send({ + code, + client_id: 'client', + grant_type: 'authorization_code', + redirect_uri: 'https://client.example.com/cb', + }) + .type('form') + .expect(200) + .expect(({ body }) => { + ({ refresh_token } = body); + }); + + ([{ oidc: { entities: { AccessToken } } }] = spy.firstCall.args); + expect(AccessToken.aud).to.include('urn:foo:bar'); + + spy = sinon.spy(); + this.provider.once('grant.success', spy); + + await this.agent.post('/token') + .send({ + refresh_token, + client_id: 'client', + grant_type: 'refresh_token', + }) + .type('form') + .expect(200) + .expect(({ body }) => { + ({ refresh_token } = body); + }); + + ([{ oidc: { entities: { AccessToken } } }] = spy.firstCall.args); + expect(AccessToken.aud).to.include('urn:foo:bar'); + }); + it('allows for multiple resources to be requested', async function () { let spy = sinon.spy(); this.provider.once('grant.success', spy); const auth = new this.AuthorizationRequest({ - response_type: 'code token', + response_type: 'code', scope: 'openid', + resource: ['https://client.example.com/api', 'https://rs.example.com'], }); let code; await this.wrap({ route: '/auth', verb: 'get', auth }) .expect(302) - .expect(auth.validateFragment) - .expect(auth.validatePresence(['code', 'state', 'access_token', 'expires_in', 'token_type'])) + .expect(auth.validatePresence(['code', 'state'])) .expect(auth.validateState) .expect(auth.validateClientLocation) .expect(({ headers: { location } }) => { @@ -305,12 +569,12 @@ describe('features.resourceIndicators', () => { let AccessToken; let refresh_token; await this.agent.post('/token') - .send(`${stringify({ + .send({ code, client_id: 'client', grant_type: 'authorization_code', redirect_uri: 'https://client.example.com/cb', - })}&resource=${encodeURIComponent('https://client.example.com/api')}&resource=${encodeURIComponent('https://rs.example.com')}`) + }) .type('form') .expect(200) .expect(({ body }) => { @@ -325,11 +589,11 @@ describe('features.resourceIndicators', () => { this.provider.once('grant.success', spy); await this.agent.post('/token') - .send(`${stringify({ + .send({ refresh_token, client_id: 'client', grant_type: 'refresh_token', - })}&resource=${encodeURIComponent('https://client.example.com/api')}&resource=${encodeURIComponent('https://rs.example.com')}`) + }) .type('form') .expect(200) .expect(({ body }) => { @@ -346,15 +610,15 @@ describe('features.resourceIndicators', () => { this.provider.once('grant.success', spy); const auth = new this.AuthorizationRequest({ - response_type: 'code token', + response_type: 'code', scope: 'openid', + resource: 'http://client.example.com/api', }); let code; await this.wrap({ route: '/auth', verb: 'get', auth }) .expect(302) - .expect(auth.validateFragment) - .expect(auth.validatePresence(['code', 'state', 'access_token', 'expires_in', 'token_type'])) + .expect(auth.validatePresence(['code', 'state'])) .expect(auth.validateState) .expect(auth.validateClientLocation) .expect(({ headers: { location } }) => { @@ -367,13 +631,12 @@ describe('features.resourceIndicators', () => { client_id: 'client', grant_type: 'authorization_code', redirect_uri: 'https://client.example.com/cb', - resource: 'http://client.example.com/api', }) .type('form') .expect(400) .expect({ - error: 'invalid_resource', - error_description: 'resources must be https URIs', + error: 'invalid_target', + error_description: 'resources must be https URIs or URNs', }); }); }); @@ -390,41 +653,11 @@ describe('features.resourceIndicators', () => { .type('form') .expect(400) .expect({ - error: 'invalid_resource', + error: 'invalid_target', error_description: 'resource must be an absolute URI', }); }); - it('validates no query component is present in the resource (1/2)', async function () { - await this.agent.post('/token') - .send({ - client_id: 'client', - grant_type: 'client_credentials', - resource: 'https://client.example.com/api?', - }) - .type('form') - .expect(400) - .expect({ - error: 'invalid_resource', - error_description: 'resource must not contain a query component', - }); - }); - - it('validates no query component is present in the resource (2/2)', async function () { - await this.agent.post('/token') - .send({ - client_id: 'client', - grant_type: 'client_credentials', - resource: 'https://client.example.com/api?foo=bar', - }) - .type('form') - .expect(400) - .expect({ - error: 'invalid_resource', - error_description: 'resource must not contain a query component', - }); - }); - it('validates no fragment component is present in the resource (1/2)', async function () { await this.agent.post('/token') .send({ @@ -435,7 +668,7 @@ describe('features.resourceIndicators', () => { .type('form') .expect(400) .expect({ - error: 'invalid_resource', + error: 'invalid_target', error_description: 'resource must not contain a fragment component', }); }); @@ -450,7 +683,7 @@ describe('features.resourceIndicators', () => { .type('form') .expect(400) .expect({ - error: 'invalid_resource', + error: 'invalid_target', error_description: 'resource must not contain a fragment component', }); }); diff --git a/test/storage/jwt.test.js b/test/storage/jwt.test.js index 2233e0022..8316b9bdb 100644 --- a/test/storage/jwt.test.js +++ b/test/storage/jwt.test.js @@ -35,12 +35,13 @@ if (FORMAT === 'jwt') { const userCode = '1384-3217'; const deviceInfo = { foo: 'bar' }; const s256 = '_gPMqAT8BELhXwBa2nIT0OvdWtQCiF_g09nAyHhgCe0'; + const resource = 'urn:foo:bar'; /* eslint-disable object-property-newline */ const fullPayload = { accountId, claims, clientId, grantId, scope, sid, consumed, acr, amr, authTime, nonce, redirectUri, codeChallenge, codeChallengeMethod, aud, error, errorDescription, params, - userCode, deviceInfo, gty, + userCode, deviceInfo, gty, resource, 'x5t#S256': s256, }; /* eslint-enable object-property-newline */ @@ -121,6 +122,7 @@ if (FORMAT === 'jwt') { kind, nonce, redirectUri, + resource, scope, sid, }); @@ -161,6 +163,7 @@ if (FORMAT === 'jwt') { jwt: string, kind, nonce, + resource, scope, sid, }); @@ -207,6 +210,7 @@ if (FORMAT === 'jwt') { kind, nonce, params, + resource, scope, sid, userCode, diff --git a/test/storage/legacy.test.js b/test/storage/legacy.test.js index 86cbc52a8..7f22bde68 100644 --- a/test/storage/legacy.test.js +++ b/test/storage/legacy.test.js @@ -35,12 +35,13 @@ if (FORMAT === 'legacy') { const userCode = '1384-3217'; const deviceInfo = { foo: 'bar' }; const s256 = '_gPMqAT8BELhXwBa2nIT0OvdWtQCiF_g09nAyHhgCe0'; + const resource = 'urn:foo:bar'; /* eslint-disable object-property-newline */ const fullPayload = { accountId, claims, clientId, grantId, scope, sid, consumed, acr, amr, authTime, nonce, redirectUri, codeChallenge, codeChallengeMethod, aud, error, errorDescription, params, - userCode, deviceInfo, gty, + userCode, deviceInfo, gty, resource, 'x5t#S256': s256, }; /* eslint-enable object-property-newline */ @@ -119,6 +120,7 @@ if (FORMAT === 'legacy') { kind, nonce, redirectUri, + resource, scope, sid, }); @@ -154,6 +156,7 @@ if (FORMAT === 'legacy') { jti: upsert.getCall(0).args[0], kind, nonce, + resource, scope, sid, }); @@ -196,6 +199,7 @@ if (FORMAT === 'legacy') { kind, nonce, params, + resource, scope, sid, userCode, diff --git a/test/storage/opaque.test.js b/test/storage/opaque.test.js index 81e7fc92b..1ab81752e 100644 --- a/test/storage/opaque.test.js +++ b/test/storage/opaque.test.js @@ -29,12 +29,13 @@ if (FORMAT === 'opaque') { const userCode = '1384-3217'; const deviceInfo = { foo: 'bar' }; const s256 = '_gPMqAT8BELhXwBa2nIT0OvdWtQCiF_g09nAyHhgCe0'; + const resource = 'urn:foo:bar'; /* eslint-disable object-property-newline */ const fullPayload = { accountId, claims, clientId, grantId, scope, sid, consumed, acr, amr, authTime, nonce, redirectUri, codeChallenge, codeChallengeMethod, aud, error, errorDescription, params, - userCode, deviceInfo, gty, + userCode, deviceInfo, gty, resource, 'x5t#S256': s256, }; /* eslint-enable object-property-newline */ @@ -97,6 +98,7 @@ if (FORMAT === 'opaque') { kind, nonce, redirectUri, + resource, scope, sid, }); @@ -130,6 +132,7 @@ if (FORMAT === 'opaque') { kind, nonce, params, + resource, scope, sid, userCode, @@ -158,6 +161,7 @@ if (FORMAT === 'opaque') { jti: upsert.getCall(0).args[0], kind, nonce, + resource, scope, sid, });