Skip to content

Commit

Permalink
feat: update DPoP implementation to indivudal draft 03
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Oct 31, 2019
1 parent afbdfec commit a7f5d7d
Show file tree
Hide file tree
Showing 22 changed files with 89 additions and 89 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The following draft specifications are implemented by oidc-provider.
- [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens - draft 02][jwt-at]
- [JWT Response for OAuth Token Introspection - draft 08][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) - individual draft 02][dpop]
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - individual draft 03][dpop]
- [OAuth 2.0 JWT Secured Authorization Request (JAR)][jar]
- [OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens (MTLS) - draft 17][mtls]
- [OAuth 2.0 Pushed Authorization Requests - draft 00][par]
Expand Down Expand Up @@ -198,7 +198,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-17
[dpop]: https://tools.ietf.org/html/draft-fett-oauth-dpop-02
[dpop]: https://tools.ietf.org/html/draft-fett-oauth-dpop-03
[resource-indicators]: https://tools.ietf.org/html/draft-ietf-oauth-resource-indicators-07
[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-02
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ _**default value**_:

### features.dPoP

[draft-fett-oauth-dpop-02](https://tools.ietf.org/html/draft-fett-oauth-dpop-02) - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer
[draft-fett-oauth-dpop-03](https://tools.ietf.org/html/draft-fett-oauth-dpop-03) - 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

Expand Down
2 changes: 1 addition & 1 deletion example/my_adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class MyAdapter {
* - sid {string} - session identifier the token comes from
* - 'x5t#S256' {string} - X.509 Certificate SHA-256 Thumbprint of a certificate bound access or
* refresh token
* - 'jkt#S256' {string} - JWK SHA-256 Thumbprint (according to [RFC7638]) of a DPoP bound
* - 'jkt' {string} - JWK SHA-256 Thumbprint (according to [RFC7638]) of a DPoP bound
* access or refresh token
* - gty {string} - [AccessToken, RefreshToken only] space delimited grant values, indicating
* the grant type(s) they originate from (implicit, authorization_code, refresh_token or
Expand Down
4 changes: 2 additions & 2 deletions lib/actions/grants/authorization_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ module.exports.handler = async function authorizationCodeHandler(ctx, next) {
});

if (ctx.oidc.client.tokenEndpointAuthMethod === 'none') {
if (at['jkt#S256']) {
rt['jkt#S256'] = at['jkt#S256'];
if (at.jkt) {
rt.jkt = at.jkt;
}

if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
Expand Down
4 changes: 2 additions & 2 deletions lib/actions/grants/device_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ module.exports.handler = async function deviceCodeHandler(ctx, next) {
});

if (ctx.oidc.client.tokenEndpointAuthMethod === 'none') {
if (at['jkt#S256']) {
rt['jkt#S256'] = at['jkt#S256'];
if (at.jkt) {
rt.jkt = at.jkt;
}

if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
Expand Down
6 changes: 3 additions & 3 deletions lib/actions/grants/refresh_token.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ module.exports.handler = async function refreshTokenHandler(ctx, next) {
ctx.assert(unique, new InvalidGrant('DPoP Token Replay detected'));
}

if (refreshToken['jkt#S256'] && (!dPoP || refreshToken['jkt#S256'] !== dPoP.jwk.thumbprint)) {
throw new InvalidGrant('failed jkt#S256 verification');
if (refreshToken.jkt && (!dPoP || refreshToken.jkt !== dPoP.jwk.thumbprint)) {
throw new InvalidGrant('failed jkt verification');
}

ctx.oidc.entity('RefreshToken', refreshToken);
Expand Down Expand Up @@ -126,7 +126,7 @@ module.exports.handler = async function refreshTokenHandler(ctx, next) {
sessionUid: refreshToken.sessionUid,
sid: refreshToken.sid,
'x5t#S256': refreshToken['x5t#S256'],
'jkt#S256': refreshToken['jkt#S256'],
jkt: refreshToken.jkt,
});

if (refreshToken.gty && !refreshToken.gty.endsWith(gty)) {
Expand Down
4 changes: 2 additions & 2 deletions lib/actions/introspection.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ module.exports = function introspectionAction(provider) {
ctx.body.cnf['x5t#S256'] = token['x5t#S256'];
}

if (token['jkt#S256']) {
ctx.body.cnf['jkt#S256'] = token['jkt#S256'];
if (token.jkt) {
ctx.body.cnf.jkt = token.jkt;
}

await next();
Expand Down
6 changes: 3 additions & 3 deletions lib/actions/userinfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ module.exports = [
if (err.expose) {
let scheme;

if (/dpop/i.test(err.error_description) || (ctx.oidc.accessToken && ctx.oidc.accessToken['jkt#S256'])) {
if (/dpop/i.test(err.error_description) || (ctx.oidc.accessToken && ctx.oidc.accessToken.jkt)) {
scheme = 'DPoP';
} else {
scheme = 'Bearer';
Expand Down Expand Up @@ -103,8 +103,8 @@ module.exports = [
ctx.assert(unique, new InvalidToken('DPoP Token Replay detected'));
}

if (accessToken['jkt#S256'] && (!dPoP || accessToken['jkt#S256'] !== dPoP.jwk.thumbprint)) {
throw new InvalidToken('failed jkt#S256 verification');
if (accessToken.jkt && (!dPoP || accessToken.jkt !== dPoP.jwk.thumbprint)) {
throw new InvalidToken('failed jkt verification');
}

await next();
Expand Down
2 changes: 1 addition & 1 deletion lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ const DEFAULTS = {
/*
* features.dPoP
*
* title: [draft-fett-oauth-dpop-02](https://tools.ietf.org/html/draft-fett-oauth-dpop-02) - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer
* title: [draft-fett-oauth-dpop-03](https://tools.ietf.org/html/draft-fett-oauth-dpop-03) - 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
Expand Down
4 changes: 2 additions & 2 deletions lib/helpers/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ const DRAFTS = new Map(Object.entries({
dPoP: {
name: 'OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer',
type: 'Individual draft',
url: 'https://tools.ietf.org/html/draft-fett-oauth-dpop-02',
version: 'id-02',
url: 'https://tools.ietf.org/html/draft-fett-oauth-dpop-03',
version: 'id-03',
},
frontchannelLogout: {
name: 'OpenID Connect Front-Channel Logout 1.0 - draft 02',
Expand Down
8 changes: 4 additions & 4 deletions lib/helpers/oidc_context.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,11 @@ module.exports = function getContext(provider) {
if (typeof payload.iat !== 'number' || !payload.iat) {
throw new Error('must have a iat number property');
}
if (payload.http_method !== this.ctx.method) {
throw new Error('http_method mismatch');
if (payload.htm !== this.ctx.method) {
throw new Error('htm mismatch');
}
if (payload.http_uri !== `${this.ctx.origin}${this.ctx.path}`) {
throw new Error('http_uri mismatch');
if (payload.htu !== `${this.ctx.origin}${this.ctx.path}`) {
throw new Error('htu mismatch');
}

try {
Expand Down
4 changes: 2 additions & 2 deletions lib/models/formats/jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ module.exports = (provider, { opaque }) => {
async getValueAndPayload() {
const [, payload] = await opaque.getValueAndPayload.call(this);
const {
jti, iat, exp, scope, aud, clientId: azp, 'x5t#S256': x5t, 'jkt#S256': jkt, extra,
jti, iat, exp, scope, aud, clientId: azp, 'x5t#S256': x5t, jkt, extra,
} = payload;
let { accountId: sub } = payload;

Expand Down Expand Up @@ -82,7 +82,7 @@ module.exports = (provider, { opaque }) => {
tokenPayload.cnf['x5t#S256'] = x5t;
}
if (jkt) {
tokenPayload.cnf['jkt#S256'] = jkt;
tokenPayload.cnf.jkt = jkt;
}

const structuredToken = {
Expand Down
4 changes: 2 additions & 2 deletions lib/models/formats/jwt_ietf.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = (provider, { opaque, jwt }) => ({
async getValueAndPayload() {
const [, payload] = await opaque.getValueAndPayload.call(this);
const {
jti, iat, exp, scope, clientId, 'x5t#S256': x5t, 'jkt#S256': jkt, extra,
jti, iat, exp, scope, clientId, 'x5t#S256': x5t, jkt, extra,
} = payload;
let { aud, accountId: sub } = payload;

Expand Down Expand Up @@ -59,7 +59,7 @@ module.exports = (provider, { opaque, jwt }) => ({
tokenPayload.cnf['x5t#S256'] = x5t;
}
if (jkt) {
tokenPayload.cnf['jkt#S256'] = jkt;
tokenPayload.cnf.jkt = jkt;
}

const structuredToken = {
Expand Down
4 changes: 2 additions & 2 deletions lib/models/formats/paseto.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module.exports = (provider, { opaque }) => {
}
const [, payload] = await opaque.getValueAndPayload.call(this);
const {
jti, iat, exp, scope, clientId, 'x5t#S256': x5t, 'jkt#S256': jkt, extra,
jti, iat, exp, scope, clientId, 'x5t#S256': x5t, jkt, extra,
} = payload;
let { aud, accountId: sub } = payload;

Expand Down Expand Up @@ -84,7 +84,7 @@ module.exports = (provider, { opaque }) => {
tokenPayload.cnf['x5t#S256'] = x5t;
}
if (jkt) {
tokenPayload.cnf['jkt#S256'] = jkt;
tokenPayload.cnf.jkt = jkt;
}

const structuredToken = {
Expand Down
2 changes: 1 addition & 1 deletion lib/models/mixins/is_sender_constrained.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const x5t = 'x5t#S256';
const jkt = 'jkt#S256';
const jkt = 'jkt';

const { [x5t]: thumbprint } = require('../../helpers/calculate_thumbprint');

Expand Down
48 changes: 24 additions & 24 deletions test/dpop/dpop.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('features.dPoP', () => {
});
});
before(function () {
this.proof = (uri, method, jwk = this.jwk) => JWT.sign({ http_uri: uri, http_method: method, jti: nanoid() }, jwk, { kid: false, header: { typ: 'dpop+jwt', jwk: JWK.asKey(jwk) } });
this.proof = (uri, method, jwk = this.jwk) => JWT.sign({ htu: uri, htm: method, jti: nanoid() }, jwk, { kid: false, header: { typ: 'dpop+jwt', jwk: JWK.asKey(jwk) } });
});

it('validates the way DPoP Proof JWT is provided', async function () {
Expand Down Expand Up @@ -125,28 +125,28 @@ describe('features.dPoP', () => {
}

await this.agent.get('/me') // eslint-disable-line no-await-in-loop
.set('DPoP', JWT.sign({ jti: 'foo', http_method: 'POST' }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key } }))
.set('DPoP', JWT.sign({ jti: 'foo', htm: 'POST' }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key } }))
.set('Authorization', 'DPoP foo')
.expect(400)
.expect({ error: 'invalid_request', error_description: 'invalid DPoP Proof JWT (http_method mismatch)' });
.expect({ error: 'invalid_request', error_description: 'invalid DPoP Proof JWT (htm mismatch)' });

await this.agent.get('/me') // eslint-disable-line no-await-in-loop
.set('DPoP', JWT.sign({ jti: 'foo', http_method: 'GET', http_uri: 'foo' }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key } }))
.set('DPoP', JWT.sign({ jti: 'foo', htm: 'GET', htu: 'foo' }, key, { kid: false, header: { typ: 'dpop+jwt', jwk: key } }))
.set('Authorization', 'DPoP foo')
.expect(400)
.expect({ error: 'invalid_request', error_description: 'invalid DPoP Proof JWT (http_uri mismatch)' });
.expect({ error: 'invalid_request', error_description: 'invalid DPoP Proof JWT (htu mismatch)' });

await this.agent.get('/me') // eslint-disable-line no-await-in-loop
.set('DPoP', JWT.sign({
jti: 'foo', http_method: 'GET', http_uri: `${this.provider.issuer}/me`, iat: epochTime() - 61,
jti: 'foo', htm: 'GET', htu: `${this.provider.issuer}/me`, iat: epochTime() - 61,
}, key, { kid: false, iat: false, header: { typ: 'dpop+jwt', jwk: key } }))
.set('Authorization', 'DPoP foo')
.expect(400)
.expect({ error: 'invalid_request', error_description: 'invalid DPoP Proof JWT (failed claim check (maxTokenAge exceeded))' });

await this.agent.get('/me') // eslint-disable-line no-await-in-loop
.set('DPoP', JWT.sign({
jti: 'foo', http_method: 'GET', http_uri: `${this.provider.issuer}/me`,
jti: 'foo', htm: 'GET', htu: `${this.provider.issuer}/me`,
}, key, { kid: false, header: { typ: 'dpop+jwt', jwk: await JWK.generate('EC') } }))
.set('Authorization', 'DPoP foo')
.expect(400)
Expand Down Expand Up @@ -194,7 +194,7 @@ describe('features.dPoP', () => {
.expect(401);

expect(spy).to.have.property('calledOnce', true);
expect(spy.args[0][1]).to.have.property('error_detail', 'failed jkt#S256 verification');
expect(spy.args[0][1]).to.have.property('error_detail', 'failed jkt verification');

spy = sinon.spy();
this.provider.once('userinfo.error', spy);
Expand All @@ -205,7 +205,7 @@ describe('features.dPoP', () => {
.expect(401);

expect(spy).to.have.property('calledOnce', true);
expect(spy.args[0][1]).to.have.property('error_detail', 'failed jkt#S256 verification');
expect(spy.args[0][1]).to.have.property('error_detail', 'failed jkt verification');
});
});

Expand All @@ -228,7 +228,7 @@ describe('features.dPoP', () => {
.expect(({ body }) => {
expect(body).to.have.property('cnf');
expect(body).to.have.property('token_type', 'DPoP');
expect(body.cnf).to.have.property('jkt#S256');
expect(body.cnf).to.have.property('jkt');
});
});
});
Expand Down Expand Up @@ -266,8 +266,8 @@ describe('features.dPoP', () => {

expect(spy).to.have.property('calledOnce', true);
const { oidc: { entities: { AccessToken, RefreshToken } } } = spy.args[0][0];
expect(AccessToken).to.have.property('jkt#S256', expectedS256);
expect(RefreshToken).not.to.have.property('jkt#S256');
expect(AccessToken).to.have.property('jkt', expectedS256);
expect(RefreshToken).not.to.have.property('jkt');
});

it('binds the refresh token to the jwk for public clients', async function () {
Expand All @@ -291,8 +291,8 @@ describe('features.dPoP', () => {

expect(spy).to.have.property('calledOnce', true);
const { oidc: { entities: { AccessToken, RefreshToken } } } = spy.args[0][0];
expect(AccessToken).to.have.property('jkt#S256', expectedS256);
expect(RefreshToken).to.have.property('jkt#S256', expectedS256);
expect(AccessToken).to.have.property('jkt', expectedS256);
expect(RefreshToken).to.have.property('jkt', expectedS256);
});
});

Expand Down Expand Up @@ -334,8 +334,8 @@ describe('features.dPoP', () => {

expect(spy).to.have.property('calledOnce', true);
const { oidc: { entities: { AccessToken, RefreshToken } } } = spy.args[0][0];
expect(AccessToken).to.have.property('jkt#S256', expectedS256);
expect(RefreshToken).not.to.have.property('jkt#S256');
expect(AccessToken).to.have.property('jkt', expectedS256);
expect(RefreshToken).not.to.have.property('jkt');
});
});

Expand Down Expand Up @@ -371,8 +371,8 @@ describe('features.dPoP', () => {

expect(spy).to.have.property('calledOnce', true);
const { oidc: { entities: { AccessToken, RefreshToken } } } = spy.args[0][0];
expect(AccessToken).to.have.property('jkt#S256', expectedS256);
expect(RefreshToken['jkt#S256']).to.be.undefined;
expect(AccessToken).to.have.property('jkt', expectedS256);
expect(RefreshToken.jkt).to.be.undefined;
});
});
});
Expand Down Expand Up @@ -416,8 +416,8 @@ describe('features.dPoP', () => {

expect(spy).to.have.property('calledOnce', true);
const { oidc: { entities: { AccessToken, RefreshToken } } } = spy.args[0][0];
expect(AccessToken).to.have.property('jkt#S256', expectedS256);
expect(RefreshToken).to.have.property('jkt#S256', expectedS256);
expect(AccessToken).to.have.property('jkt', expectedS256);
expect(RefreshToken).to.have.property('jkt', expectedS256);
});
});

Expand Down Expand Up @@ -453,8 +453,8 @@ describe('features.dPoP', () => {

expect(spy).to.have.property('calledOnce', true);
const { oidc: { entities: { AccessToken, RefreshToken } } } = spy.args[0][0];
expect(AccessToken).to.have.property('jkt#S256', expectedS256);
expect(RefreshToken).to.have.property('jkt#S256', expectedS256);
expect(AccessToken).to.have.property('jkt', expectedS256);
expect(RefreshToken).to.have.property('jkt', expectedS256);
});

it('verifies the request made with the same cert jwk', async function () {
Expand All @@ -474,7 +474,7 @@ describe('features.dPoP', () => {
.expect({ error: 'invalid_grant', error_description: 'grant request is invalid' });

expect(spy).to.have.property('calledOnce', true);
expect(spy.args[0][1]).to.have.property('error_detail', 'failed jkt#S256 verification');
expect(spy.args[0][1]).to.have.property('error_detail', 'failed jkt verification');
});
});
});
Expand All @@ -493,7 +493,7 @@ describe('features.dPoP', () => {

expect(spy).to.have.property('calledOnce', true);
const { oidc: { entities: { ClientCredentials } } } = spy.args[0][0];
expect(ClientCredentials).to.have.property('jkt#S256', expectedS256);
expect(ClientCredentials).to.have.property('jkt', expectedS256);
});
});
});
Loading

0 comments on commit a7f5d7d

Please sign in to comment.