Skip to content

Commit

Permalink
feat: update DPoP implementation to ietf draft 01
Browse files Browse the repository at this point in the history
BREAKING CHANGE: DPoP implementation updated to
[draft-ietf-oauth-dpop-01](https://tools.ietf.org/html/draft-ietf-oauth-dpop-01)

Note: Updates to draft specification versions are released as MINOR
library versions, if you utilize these specification implementations
consider using the tilde `~` operator in your package.json since
breaking changes may be introduced as part of these version updates.
Alternatively, [acknowledge](/docs/README.md#features) the version and
be notified of breaking changes as part of your CI.
  • Loading branch information
panva committed May 5, 2020
1 parent 3d09296 commit 330d13c
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 94 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The following draft specifications are implemented by oidc-provider.
- [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens - draft 05][jwt-at]
- [JWT Response for OAuth Token Introspection - draft 09][jwt-introspection]
- [JWT Secured Authorization Response Mode for OAuth 2.0 (JARM) - draft 02][jarm]
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 00][dpop]
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 01][dpop]
- [OAuth 2.0 JWT Secured Authorization Request (JAR)][jar]
- [OAuth 2.0 Pushed Authorization Requests - draft 01][par]
- [OAuth 2.0 Resource Indicators - draft 08][resource-indicators]
Expand Down Expand Up @@ -169,7 +169,7 @@ See the list of available emitted [event names](/docs/events.md) and their descr
[jwt-introspection]: https://tools.ietf.org/html/draft-ietf-oauth-jwt-introspection-response-09
[sponsor-auth0]: https://auth0.com/overview?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=oidc-provider&utm_content=auth
[mtls]: https://tools.ietf.org/html/rfc8705
[dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-00
[dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-01
[resource-indicators]: https://tools.ietf.org/html/draft-ietf-oauth-resource-indicators-08
[jarm]: https://openid.net/specs/openid-financial-api-jarm-wd-02.html
[jwt-at]: https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-05
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ _**default value**_:

### features.dPoP

[draft-ietf-oauth-dpop-00](https://tools.ietf.org/html/draft-ietf-oauth-dpop-00) - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer
[draft-ietf-oauth-dpop-01](https://tools.ietf.org/html/draft-ietf-oauth-dpop-01) - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer

Enables `DPoP` - mechanism for sender-constraining tokens via a proof-of-possession mechanism on the application level. Browser DPoP Proof generation [here](https://www.npmjs.com/package/dpop).

Expand Down
4 changes: 4 additions & 0 deletions lib/actions/discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ module.exports = function discovery(ctx, next) {
ctx.body.introspection_signing_alg_values_supported = config.introspectionSigningAlgValues;
}

if (features.dPoP.enabled) {
ctx.body.dpop_signing_alg_values_supported = config.dPoPSigningAlgValues;
}

if (features.revocation.enabled) {
ctx.body.revocation_endpoint = ctx.oidc.urlFor('revocation');
ctx.body.revocation_endpoint_auth_methods_supported = [...config.revocationEndpointAuthMethods];
Expand Down
9 changes: 8 additions & 1 deletion lib/actions/userinfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const Debug = require('debug');
const debug = new Debug('oidc-provider:userinfo');
const uidToGrantId = new Debug('oidc-provider:uid');

const { InvalidToken, InvalidScope } = require('../helpers/errors');
const { InvalidDpopProof, InvalidToken, InvalidScope } = require('../helpers/errors');
const setWWWAuthenticate = require('../helpers/set_www_authenticate');
const bodyParser = require('../shared/conditional_body');
const rejectDupes = require('../shared/reject_dupes');
Expand Down Expand Up @@ -49,13 +49,20 @@ module.exports = [
scheme = 'Bearer';
}

if (err instanceof InvalidDpopProof) {
err.error = err.message = 'invalid_token'; // eslint-disable-line no-multi-assign
}

setWWWAuthenticate(ctx, scheme, {
realm: ctx.oidc.issuer,
...(err.error_description !== 'no access token provided' ? {
error: err.message,
error_description: err.error_description,
scope: err.scope,
} : undefined),
...(scheme === 'DPoP' ? {
algs: instance(ctx.oidc.provider).configuration('dPoPSigningAlgValues').join(' '),
} : undefined),
});
}
throw err;
Expand Down
2 changes: 1 addition & 1 deletion lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ const DEFAULTS = {
/*
* features.dPoP
*
* title: [draft-ietf-oauth-dpop-00](https://tools.ietf.org/html/draft-ietf-oauth-dpop-00) - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer
* title: [draft-ietf-oauth-dpop-01](https://tools.ietf.org/html/draft-ietf-oauth-dpop-01) - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer
*
* description: Enables `DPoP` - mechanism for sender-constraining tokens via a
* proof-of-possession mechanism on the application level. Browser DPoP Proof generation
Expand Down
1 change: 1 addition & 0 deletions lib/helpers/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const classes = [
['expired_token'],
['interaction_required'],
['invalid_client'],
['invalid_dpop_proof'],
['invalid_request_object'],
['invalid_request_uri'],
['invalid_software_statement'],
Expand Down
4 changes: 2 additions & 2 deletions lib/helpers/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ const DRAFTS = new Map(Object.entries({
dPoP: {
name: 'OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer',
type: 'IETF OAuth Working Group draft',
url: 'https://tools.ietf.org/html/draft-ietf-oauth-dpop-00',
version: ['id-03', 'individual-draft-03', 'individual-draft-04', 'draft-00'],
url: 'https://tools.ietf.org/html/draft-ietf-oauth-dpop-01',
version: ['draft-01'],
},
frontchannelLogout: {
name: 'OpenID Connect Front-Channel Logout 1.0 - draft 02',
Expand Down
57 changes: 19 additions & 38 deletions lib/helpers/oidc_context.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ const omitBy = require('lodash/omitBy');
const get = require('lodash/get');
const isUndefined = require('lodash/isUndefined');
const debug = require('debug')('oidc-provider:bearer');
const { JWT, JWK, errors } = require('jose');
const { JWT, JWK } = require('jose');

const ctxRef = require('../models/ctx_ref');

const nanoid = require('./nanoid');
const { InvalidRequest } = require('./errors');
const { InvalidRequest, InvalidDpopProof } = require('./errors');
const instance = require('./weak_cache');
const resolveResponseMode = require('./resolve_response_mode');

Expand Down Expand Up @@ -122,58 +122,39 @@ module.exports = function getContext(provider) {
}

try {
const { header, payload } = JWT.decode(token, { complete: true });
let key;
const { payload, key } = JWT.verify(
token,
JWK.EmbeddedJWK,
{
maxTokenAge: `${dPoPConfig.iatTolerance} seconds`,
clockTolerance: `${clockTolerance} seconds`,
algorithms: instance(provider).configuration('dPoPSigningAlgValues'),
complete: true,
typ: 'dpop+jwt',
},
);

if (header.typ !== 'dpop+jwt') {
throw new Error('typ must be dpop+jwt');
}
if (typeof header.alg !== 'string' || !header.alg || header.alg === 'none' || header.alg.startsWith('HS')) {
throw new Error('invalid alg');
}
if (!instance(provider).configuration('dPoPSigningAlgValues').includes(header.alg)) {
throw new Error('unsupported alg');
}
if (typeof header.jwk !== 'object' || !header.jwk) {
throw new Error('header must have a jwk');
}
try {
key = JWK.asKey(header.jwk);
} catch (err) {
throw new Error('failed to import jwk');
}
if (key.type !== 'public') {
throw new Error('jwk must be a public key');
}
if (typeof payload.jti !== 'string' || !payload.jti) {
throw new Error('must have a jti string property');
}
if (typeof payload.iat !== 'number' || !payload.iat) {
throw new Error('must have a iat number property');
}
if (payload.htm !== this.ctx.method) {

// HTTP Methods are case-insensitive
if (String(payload.htm).toLowerCase() !== this.ctx.method.toLowerCase()) {
throw new Error('htm mismatch');
}

// TODO: allow trailing slash to be added/omitted at will,
// see https://github.com/danielfett/draft-dpop/issues/49
if (payload.htu !== this.urlFor(this.route)) {
throw new Error('htu mismatch');
}

try {
JWT.verify(token, key, { maxTokenAge: `${dPoPConfig.iatTolerance} seconds`, clockTolerance: `${clockTolerance} seconds` });
} catch (err) {
if (err instanceof errors.JWTClaimInvalid) {
throw new Error(`failed claim check (${err.message})`);
}
throw err;
}

const result = { jwk: key, jti: payload.jti, iat: payload.iat };
instance(this).dpop = result;

return result;
} catch (err) {
throw new InvalidRequest(`invalid DPoP Proof JWT (${err.message})`);
throw new InvalidDpopProof(`invalid DPoP key binding (${err.message})`);
}
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"debug": "^4.1.1",
"ejs": "^3.0.1",
"got": "^9.6.0",
"jose": "^1.25.2",
"jose": "^1.27.0",
"jsesc": "^3.0.1",
"koa": "^2.11.0",
"koa-compose": "^4.1.0",
Expand Down
1 change: 1 addition & 0 deletions test/dpop/dpop.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const merge = require('lodash/merge');

const config = cloneDeep(require('../default.config'));

config.whitelistedJWA.dPoPSigningAlgValues = ['ES256', 'PS256'];
merge(config.features, {
dPoP: { enabled: true },
clientCredentials: { enabled: true },
Expand Down
Loading

0 comments on commit 330d13c

Please sign in to comment.