Skip to content

Commit 917507f

Browse files
committed
feat: bump DPoP to draft-11
1 parent 572fbae commit 917507f

24 files changed

+475
-116
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ The following draft specifications are implemented by oidc-provider:
4949

5050
- [JWT Response for OAuth Token Introspection - draft 10][jwt-introspection]
5151
- [Financial-grade API: Client Initiated Backchannel Authentication Profile (FAPI-CIBA) - Implementer's Draft 01][fapi-ciba]
52-
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 03][dpop]
52+
- [OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP) - draft 11][dpop]
5353

5454
Updates to draft specification versions are released as MINOR library versions,
5555
if you utilize these specification implementations consider using the tilde `~` operator in your
@@ -139,7 +139,7 @@ actions and i.e. emit metrics that react to specific triggers. See the list of a
139139
[jwt-introspection]: https://tools.ietf.org/html/draft-ietf-oauth-jwt-introspection-response-10
140140
[sponsor-auth0]: https://a0.to/try-auth0
141141
[mtls]: https://www.rfc-editor.org/rfc/rfc8705.html
142-
[dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-03
142+
[dpop]: https://tools.ietf.org/html/draft-ietf-oauth-dpop-11
143143
[resource-indicators]: https://www.rfc-editor.org/rfc/rfc8707.html
144144
[jarm]: https://openid.net/specs/oauth-v2-jarm.html
145145
[jwt-at]: https://www.rfc-editor.org/rfc/rfc9068.html

docs/README.md

+4-5
Original file line numberDiff line numberDiff line change
@@ -848,9 +848,9 @@ _**default value**_:
848848

849849
### features.dPoP
850850

851-
[draft-ietf-oauth-dpop-03](https://tools.ietf.org/html/draft-ietf-oauth-dpop-03) - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP)
851+
[draft-ietf-oauth-dpop-11](https://tools.ietf.org/html/draft-ietf-oauth-dpop-11) - OAuth 2.0 Demonstration of Proof-of-Possession at the Application Layer (DPoP)
852852

853-
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).
853+
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).
854854

855855

856856
_**recommendation**_: 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](#features) the version and be notified of breaking changes as part of your CI.
@@ -860,8 +860,7 @@ _**default value**_:
860860
```js
861861
{
862862
ack: undefined,
863-
enabled: false,
864-
iatTolerance: 60
863+
enabled: false
865864
}
866865
```
867866

@@ -3092,7 +3091,7 @@ _**default value**_:
30923091

30933092
### enabledJWA.dPoPSigningAlgValues
30943093

3095-
JWS "alg" Algorithm values the provider supports to verify signed DPoP Proof JWTs with
3094+
JWS "alg" Algorithm values the provider supports to verify signed DPoP proof JWTs with
30963095

30973096

30983097

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const { InvalidRequest } = require('../../helpers/errors');
2+
const dpopValidate = require('../../helpers/validate_dpop');
3+
const epochTime = require('../../helpers/epoch_time');
4+
5+
/*
6+
* Validates dpop_jkt equals the used DPoP proof thumbprint
7+
* when provided, otherwise defaults dpop_jkt to it.
8+
*
9+
* @throws: invalid_request
10+
*/
11+
module.exports = async function checkDpopJkt(ctx, next) {
12+
const { params } = ctx.oidc;
13+
14+
const dPoP = await dpopValidate(ctx);
15+
if (dPoP) {
16+
const { ReplayDetection } = ctx.oidc.provider;
17+
const unique = await ReplayDetection.unique(
18+
ctx.oidc.client.clientId,
19+
dPoP.jti,
20+
epochTime() + 60,
21+
);
22+
23+
ctx.assert(unique, new InvalidRequest('DPoP proof JWT Replay detected'));
24+
25+
if (params.dpop_jkt && params.dpop_jkt !== dPoP.thumbprint) {
26+
throw new InvalidRequest('DPoP proof key thumbprint does not match dpop_jkt');
27+
} else if (!params.dpop_jkt) {
28+
params.dpop_jkt = dPoP.thumbprint;
29+
}
30+
}
31+
32+
return next();
33+
};

lib/actions/authorization/index.js

+7
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const cibaLoadAccount = require('./ciba_load_account');
5151
const checkRequestedExpiry = require('./check_requested_expiry');
5252
const backchannelRequestResponse = require('./backchannel_request_response');
5353
const checkCibaContext = require('./check_ciba_context');
54+
const checkDpopJkt = require('./check_dpop_jkt');
5455

5556
const A = 'authorization';
5657
const R = 'resume';
@@ -68,6 +69,7 @@ module.exports = function authorizationAction(provider, endpoint) {
6869
const {
6970
features: {
7071
claimsParameter,
72+
dPoP,
7173
resourceIndicators,
7274
webMessageResponseMode,
7375
},
@@ -113,6 +115,10 @@ module.exports = function authorizationAction(provider, endpoint) {
113115
allowList.add('requested_expiry');
114116
}
115117

118+
if (dPoP && [A, R, PAR].includes(endpoint)) {
119+
allowList.add('dpop_jkt');
120+
}
121+
116122
const stack = [];
117123

118124
const use = (middleware, ...only) => {
@@ -169,6 +175,7 @@ module.exports = function authorizationAction(provider, endpoint) {
169175
use(() => checkRequestedExpiry, BA);
170176
use(() => checkCibaContext, BA);
171177
use(() => checkIdTokenHint, A, DA, PAR );
178+
use(() => checkDpopJkt, PAR );
172179
use(() => interactionEmit, A, R, CV, DR );
173180
use(() => assignClaims, A, R, CV, DR, BA);
174181
use(() => cibaLoadAccount, BA);

lib/actions/authorization/process_request_object.js

+4
Original file line numberDiff line numberDiff line change
@@ -275,5 +275,9 @@ module.exports = async function processRequestObject(PARAM_LIST, rejectDupesMidd
275275
default:
276276
}
277277

278+
if (pushedRequestObject && ctx.oidc.entities.PushedAuthorizationRequest.dpopJkt) {
279+
params.dpop_jkt = ctx.oidc.entities.PushedAuthorizationRequest.dpopJkt;
280+
}
281+
278282
return next();
279283
};

lib/actions/authorization/process_response_types.js

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ async function codeHandler(ctx) {
8484
resource: Object.keys(ctx.oidc.resourceServers),
8585
scope: [...scopeSet].join(' '),
8686
sessionUid: ctx.oidc.session.uid,
87+
dpopJkt: ctx.oidc.params.dpop_jkt,
8788
});
8889

8990
if (Object.keys(code.claims).length === 0) {

lib/actions/authorization/pushed_authorization_request_response.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ const MAX_TTL = 60;
99
module.exports = async function pushedAuthorizationRequestResponse(ctx, next) {
1010
let request;
1111
let ttl;
12+
let dpopJkt;
1213
const now = epochTime();
1314
if (ctx.oidc.body.request) {
1415
({ request } = ctx.oidc.body);
15-
const { payload: { exp } } = JWT.decode(request);
16+
const { payload: { exp, dpop_jkt: thumbprint } } = JWT.decode(request);
1617
ttl = exp - now;
1718

1819
if (!Number.isInteger(ttl) || ttl > MAX_TTL) {
1920
ttl = MAX_TTL;
2021
}
22+
dpopJkt = thumbprint || ctx.oidc.params.dpop_jkt;
2123
} else {
2224
ttl = MAX_TTL;
2325
request = new UnsecuredJWT({ ...ctx.oidc.params })
@@ -27,9 +29,10 @@ module.exports = async function pushedAuthorizationRequestResponse(ctx, next) {
2729
.setExpirationTime(now + MAX_TTL)
2830
.setNotBefore(now)
2931
.encode();
32+
dpopJkt = ctx.oidc.params.dpop_jkt;
3033
}
3134

32-
const requestObject = new ctx.oidc.provider.PushedAuthorizationRequest({ request });
35+
const requestObject = new ctx.oidc.provider.PushedAuthorizationRequest({ request, dpopJkt });
3336

3437
const id = await requestObject.save(ttl);
3538

lib/actions/grants/authorization_code.js

+17-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const revoke = require('../../helpers/revoke');
66
const filterClaims = require('../../helpers/filter_claims');
77
const dpopValidate = require('../../helpers/validate_dpop');
88
const resolveResource = require('../../helpers/resolve_resource');
9+
const epochTime = require('../../helpers/epoch_time');
910

1011
const gty = 'authorization_code';
1112

@@ -16,7 +17,6 @@ module.exports.handler = async function authorizationCodeHandler(ctx, next) {
1617
conformIdTokenClaims,
1718
features: {
1819
userinfo,
19-
dPoP: { iatTolerance },
2020
mTLS: { getCertificate },
2121
resourceIndicators,
2222
},
@@ -32,6 +32,8 @@ module.exports.handler = async function authorizationCodeHandler(ctx, next) {
3232

3333
presence(ctx, 'code', 'redirect_uri');
3434

35+
const dPoP = await dpopValidate(ctx);
36+
3537
const code = await ctx.oidc.provider.AuthorizationCode.find(ctx.oidc.params.code, {
3638
ignoreExpiration: true,
3739
});
@@ -70,6 +72,10 @@ module.exports.handler = async function authorizationCodeHandler(ctx, next) {
7072
}
7173
}
7274

75+
if (!dPoP && ctx.oidc.client.dpopBoundAccessTokens) {
76+
throw new InvalidGrant('DPoP proof JWT not provided');
77+
}
78+
7379
if (grant.clientId !== ctx.oidc.client.clientId) {
7480
throw new InvalidGrant('client mismatch');
7581
}
@@ -118,16 +124,22 @@ module.exports.handler = async function authorizationCodeHandler(ctx, next) {
118124
at.setThumbprint('x5t', cert);
119125
}
120126

121-
const dPoP = await dpopValidate(ctx);
127+
if (code.dpopJkt && !dPoP) {
128+
throw new InvalidGrant('missing DPoP proof JWT');
129+
}
122130

123131
if (dPoP) {
124132
const unique = await ReplayDetection.unique(
125133
ctx.oidc.client.clientId,
126134
dPoP.jti,
127-
dPoP.iat + iatTolerance,
135+
epochTime() + 60,
128136
);
129137

130-
ctx.assert(unique, new InvalidGrant('DPoP Token Replay detected'));
138+
ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
139+
140+
if (code.dpopJkt && code.dpopJkt !== dPoP.thumbprint) {
141+
throw new InvalidGrant('DPoP proof key thumbprint does not match dpop_jkt');
142+
}
131143

132144
at.setThumbprint('jkt', dPoP.thumbprint);
133145
}
@@ -172,7 +184,7 @@ module.exports.handler = async function authorizationCodeHandler(ctx, next) {
172184
rt.jkt = at.jkt;
173185
}
174186

175-
if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
187+
if (at['x5t#S256']) {
176188
rt['x5t#S256'] = at['x5t#S256'];
177189
}
178190
}

lib/actions/grants/ciba.js

+10-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const filterClaims = require('../../helpers/filter_claims');
77
const revoke = require('../../helpers/revoke');
88
const dpopValidate = require('../../helpers/validate_dpop');
99
const resolveResource = require('../../helpers/resolve_resource');
10+
const epochTime = require('../../helpers/epoch_time');
1011

1112
const {
1213
AuthorizationPending,
@@ -24,12 +25,13 @@ module.exports.handler = async function cibaHandler(ctx, next) {
2425
conformIdTokenClaims,
2526
features: {
2627
userinfo,
27-
dPoP: { iatTolerance },
2828
mTLS: { getCertificate },
2929
resourceIndicators,
3030
},
3131
} = instance(ctx.oidc.provider).configuration();
3232

33+
const dPoP = await dpopValidate(ctx);
34+
3335
const request = await ctx.oidc.provider.BackchannelAuthenticationRequest.find(
3436
ctx.oidc.params.auth_req_id,
3537
{ ignoreExpiration: true },
@@ -51,6 +53,10 @@ module.exports.handler = async function cibaHandler(ctx, next) {
5153
}
5254
}
5355

56+
if (!dPoP && ctx.oidc.client.dpopBoundAccessTokens) {
57+
throw new InvalidGrant('DPoP proof JWT not provided');
58+
}
59+
5460
if (request.isExpired) {
5561
throw new ExpiredToken('backchannel authentication request is expired');
5662
}
@@ -123,16 +129,14 @@ module.exports.handler = async function cibaHandler(ctx, next) {
123129
at.setThumbprint('x5t', cert);
124130
}
125131

126-
const dPoP = await dpopValidate(ctx);
127-
128132
if (dPoP) {
129133
const unique = await ReplayDetection.unique(
130134
ctx.oidc.client.clientId,
131135
dPoP.jti,
132-
dPoP.iat + iatTolerance,
136+
epochTime() + 60,
133137
);
134138

135-
ctx.assert(unique, new InvalidGrant('DPoP Token Replay detected'));
139+
ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
136140

137141
at.setThumbprint('jkt', dPoP.thumbprint);
138142
}
@@ -177,7 +181,7 @@ module.exports.handler = async function cibaHandler(ctx, next) {
177181
rt.jkt = at.jkt;
178182
}
179183

180-
if (ctx.oidc.client.tlsClientCertificateBoundAccessTokens) {
184+
if (at['x5t#S256']) {
181185
rt['x5t#S256'] = at['x5t#S256'];
182186
}
183187
}

lib/actions/grants/client_credentials.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@ const instance = require('../../helpers/weak_cache');
22
const { InvalidGrant, InvalidTarget, InvalidScope } = require('../../helpers/errors');
33
const dpopValidate = require('../../helpers/validate_dpop');
44
const checkResource = require('../../shared/check_resource');
5+
const epochTime = require('../../helpers/epoch_time');
56

67
module.exports.handler = async function clientCredentialsHandler(ctx, next) {
78
const { client } = ctx.oidc;
89
const { ClientCredentials, ReplayDetection } = ctx.oidc.provider;
910
const {
1011
features: {
11-
dPoP: { iatTolerance },
1212
mTLS: { getCertificate },
1313
},
1414
scopes: statics,
1515
} = instance(ctx.oidc.provider).configuration();
1616

17+
const dPoP = await dpopValidate(ctx);
18+
1719
await checkResource(ctx, () => {});
1820

1921
const scopes = ctx.oidc.params.scope ? [...new Set(ctx.oidc.params.scope.split(' '))] : [];
@@ -51,14 +53,14 @@ module.exports.handler = async function clientCredentialsHandler(ctx, next) {
5153
token.setThumbprint('x5t', cert);
5254
}
5355

54-
const dPoP = await dpopValidate(ctx);
55-
5656
if (dPoP) {
57-
const unique = await ReplayDetection.unique(client.clientId, dPoP.jti, dPoP.iat + iatTolerance);
57+
const unique = await ReplayDetection.unique(client.clientId, dPoP.jti, epochTime() + 60);
5858

59-
ctx.assert(unique, new InvalidGrant('DPoP Token Replay detected'));
59+
ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
6060

6161
token.setThumbprint('jkt', dPoP.thumbprint);
62+
} else if (ctx.oidc.client.dpopBoundAccessTokens) {
63+
throw new InvalidGrant('DPoP proof JWT not provided');
6264
}
6365

6466
ctx.oidc.entity('ClientCredentials', token);

0 commit comments

Comments
 (0)