Skip to content

Commit

Permalink
feat(DPoP): add a setting to disable DPoP Proof Replay Detection
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Feb 2, 2024
1 parent 9cd865b commit 2744fc8
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 42 deletions.
11 changes: 11 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,7 @@ _**default value**_:
```js
{
ack: undefined,
allowReplay: false,
enabled: false,
nonceSecret: undefined,
requireNonce: [Function: requireNonce] // see expanded details below
Expand All @@ -889,6 +890,16 @@ _**default value**_:
<details><summary>(Click to expand) features.dPoP options details</summary><br>


#### allowReplay

Controls whether DPoP Proof Replay Detection is used or not.


_**default value**_:
```js
false
```

#### nonceSecret

A secret value used for generating server-provided DPoP nonces. Must be a 32-byte length Buffer instance when provided.
Expand Down
18 changes: 11 additions & 7 deletions lib/actions/authorization/check_dpop_jkt.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { InvalidRequest } from '../../helpers/errors.js';
import dpopValidate, { DPOP_OK_WINDOW } from '../../helpers/validate_dpop.js';
import epochTime from '../../helpers/epoch_time.js';
import instance from '../../helpers/weak_cache.js';

/*
* Validates dpop_jkt equals the used DPoP proof thumbprint
Expand All @@ -11,14 +12,17 @@ export default async function checkDpopJkt(ctx, next) {

const dPoP = await dpopValidate(ctx);
if (dPoP) {
const { ReplayDetection } = ctx.oidc.provider;
const unique = await ReplayDetection.unique(
ctx.oidc.client.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);
const allowReplay = instance(ctx.oidc.provider).configuration('features.dPoP.allowReplay');
if (!allowReplay) {
const { ReplayDetection } = ctx.oidc.provider;
const unique = await ReplayDetection.unique(
ctx.oidc.client.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);

ctx.assert(unique, new InvalidRequest('DPoP proof JWT Replay detected'));
ctx.assert(unique, new InvalidRequest('DPoP proof JWT Replay detected'));
}

if (params.dpop_jkt && params.dpop_jkt !== dPoP.thumbprint) {
throw new InvalidRequest('DPoP proof key thumbprint does not match dpop_jkt');
Expand Down
17 changes: 10 additions & 7 deletions lib/actions/grants/authorization_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const handler = async function authorizationCodeHandler(ctx, next) {
userinfo,
mTLS: { getCertificate },
resourceIndicators,
dPoP: { allowReplay },
},
} = instance(ctx.oidc.provider).configuration();

Expand Down Expand Up @@ -129,13 +130,15 @@ export const handler = async function authorizationCodeHandler(ctx, next) {
}

if (dPoP) {
const unique = await ReplayDetection.unique(
ctx.oidc.client.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);

ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
if (!allowReplay) {
const unique = await ReplayDetection.unique(
ctx.oidc.client.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);

ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
}

if (code.dpopJkt && code.dpopJkt !== dPoP.thumbprint) {
throw new InvalidGrant('DPoP proof key thumbprint does not match dpop_jkt');
Expand Down
17 changes: 10 additions & 7 deletions lib/actions/grants/ciba.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const handler = async function cibaHandler(ctx, next) {
features: {
userinfo,
mTLS: { getCertificate },
dPoP: { allowReplay },
resourceIndicators,
},
} = instance(ctx.oidc.provider).configuration();
Expand Down Expand Up @@ -130,13 +131,15 @@ export const handler = async function cibaHandler(ctx, next) {
}

if (dPoP) {
const unique = await ReplayDetection.unique(
ctx.oidc.client.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);

ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
if (!allowReplay) {
const unique = await ReplayDetection.unique(
ctx.oidc.client.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);

ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
}

at.setThumbprint('jkt', dPoP.thumbprint);
}
Expand Down
17 changes: 10 additions & 7 deletions lib/actions/grants/client_credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const handler = async function clientCredentialsHandler(ctx, next) {
const {
features: {
mTLS: { getCertificate },
dPoP: { allowReplay },
},
scopes: statics,
} = instance(ctx.oidc.provider).configuration();
Expand Down Expand Up @@ -54,13 +55,15 @@ export const handler = async function clientCredentialsHandler(ctx, next) {
}

if (dPoP) {
const unique = await ReplayDetection.unique(
client.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);

ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
if (!allowReplay) {
const unique = await ReplayDetection.unique(
client.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);

ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
}

token.setThumbprint('jkt', dPoP.thumbprint);
} else if (ctx.oidc.client.dpopBoundAccessTokens) {
Expand Down
17 changes: 10 additions & 7 deletions lib/actions/grants/device_code.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const handler = async function deviceCodeHandler(ctx, next) {
features: {
userinfo,
mTLS: { getCertificate },
dPoP: { allowReplay },
resourceIndicators,
},
} = instance(ctx.oidc.provider).configuration();
Expand Down Expand Up @@ -129,13 +130,15 @@ export const handler = async function deviceCodeHandler(ctx, next) {
}

if (dPoP) {
const unique = await ReplayDetection.unique(
ctx.oidc.client.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);

ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
if (!allowReplay) {
const unique = await ReplayDetection.unique(
ctx.oidc.client.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);

ctx.assert(unique, new InvalidGrant('DPoP proof JWT Replay detected'));
}

at.setThumbprint('jkt', dPoP.thumbprint);
}
Expand Down
3 changes: 2 additions & 1 deletion lib/actions/grants/refresh_token.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const handler = async function refreshTokenHandler(ctx, next) {
features: {
userinfo,
mTLS: { getCertificate },
dPoP: { allowReplay },
resourceIndicators,
},
} = conf;
Expand Down Expand Up @@ -89,7 +90,7 @@ export const handler = async function refreshTokenHandler(ctx, next) {
}
}

if (dPoP) {
if (dPoP && !allowReplay) {
const unique = await ReplayDetection.unique(
client.clientId,
dPoP.jti,
Expand Down
16 changes: 10 additions & 6 deletions lib/actions/userinfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,17 @@ export default [
}

if (dPoP) {
const unique = await ctx.oidc.provider.ReplayDetection.unique(
accessToken.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);
const allowReplay = instance(ctx.oidc.provider).configuration('features.dPoP.allowReplay');

if (!allowReplay) {
const unique = await ctx.oidc.provider.ReplayDetection.unique(
accessToken.clientId,
dPoP.jti,
epochTime() + DPOP_OK_WINDOW,
);

ctx.assert(unique, new InvalidToken('DPoP proof JWT Replay detected'));
ctx.assert(unique, new InvalidToken('DPoP proof JWT Replay detected'));
}
}

if (accessToken.jkt && (!dPoP || accessToken.jkt !== dPoP.thumbprint)) {
Expand Down
6 changes: 6 additions & 0 deletions lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,12 @@ function makeDefaults() {
* description: Function used to determine whether a DPoP nonce is required or not.
*/
requireNonce,
/**
* features.dPoP.allowReplay
*
* description: Controls whether DPoP Proof Replay Detection is used or not.
*/
allowReplay: false,
},

/*
Expand Down

0 comments on commit 2744fc8

Please sign in to comment.