From 2744fc8c093ac3c9d43720f243c89ea682b7eee6 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 2 Feb 2024 21:05:43 +0100 Subject: [PATCH] feat(DPoP): add a setting to disable DPoP Proof Replay Detection --- docs/README.md | 11 +++++++++++ lib/actions/authorization/check_dpop_jkt.js | 18 +++++++++++------- lib/actions/grants/authorization_code.js | 17 ++++++++++------- lib/actions/grants/ciba.js | 17 ++++++++++------- lib/actions/grants/client_credentials.js | 17 ++++++++++------- lib/actions/grants/device_code.js | 17 ++++++++++------- lib/actions/grants/refresh_token.js | 3 ++- lib/actions/userinfo.js | 16 ++++++++++------ lib/helpers/defaults.js | 6 ++++++ 9 files changed, 80 insertions(+), 42 deletions(-) diff --git a/docs/README.md b/docs/README.md index 33167e26d..1f1af5903 100644 --- a/docs/README.md +++ b/docs/README.md @@ -880,6 +880,7 @@ _**default value**_: ```js { ack: undefined, + allowReplay: false, enabled: false, nonceSecret: undefined, requireNonce: [Function: requireNonce] // see expanded details below @@ -889,6 +890,16 @@ _**default value**_:
(Click to expand) features.dPoP options details
+#### 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. diff --git a/lib/actions/authorization/check_dpop_jkt.js b/lib/actions/authorization/check_dpop_jkt.js index f53bd6ab4..afc53605e 100644 --- a/lib/actions/authorization/check_dpop_jkt.js +++ b/lib/actions/authorization/check_dpop_jkt.js @@ -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 @@ -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'); diff --git a/lib/actions/grants/authorization_code.js b/lib/actions/grants/authorization_code.js index 6ab648927..bd681b5d7 100644 --- a/lib/actions/grants/authorization_code.js +++ b/lib/actions/grants/authorization_code.js @@ -19,6 +19,7 @@ export const handler = async function authorizationCodeHandler(ctx, next) { userinfo, mTLS: { getCertificate }, resourceIndicators, + dPoP: { allowReplay }, }, } = instance(ctx.oidc.provider).configuration(); @@ -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'); diff --git a/lib/actions/grants/ciba.js b/lib/actions/grants/ciba.js index 82d92e3ac..33dc0907f 100644 --- a/lib/actions/grants/ciba.js +++ b/lib/actions/grants/ciba.js @@ -26,6 +26,7 @@ export const handler = async function cibaHandler(ctx, next) { features: { userinfo, mTLS: { getCertificate }, + dPoP: { allowReplay }, resourceIndicators, }, } = instance(ctx.oidc.provider).configuration(); @@ -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); } diff --git a/lib/actions/grants/client_credentials.js b/lib/actions/grants/client_credentials.js index 035b06ebe..745eaa560 100644 --- a/lib/actions/grants/client_credentials.js +++ b/lib/actions/grants/client_credentials.js @@ -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(); @@ -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) { diff --git a/lib/actions/grants/device_code.js b/lib/actions/grants/device_code.js index bf9ab6e5a..0bc5f16a1 100644 --- a/lib/actions/grants/device_code.js +++ b/lib/actions/grants/device_code.js @@ -26,6 +26,7 @@ export const handler = async function deviceCodeHandler(ctx, next) { features: { userinfo, mTLS: { getCertificate }, + dPoP: { allowReplay }, resourceIndicators, }, } = instance(ctx.oidc.provider).configuration(); @@ -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); } diff --git a/lib/actions/grants/refresh_token.js b/lib/actions/grants/refresh_token.js index fdfaad1ce..5bfaaecd3 100644 --- a/lib/actions/grants/refresh_token.js +++ b/lib/actions/grants/refresh_token.js @@ -23,6 +23,7 @@ export const handler = async function refreshTokenHandler(ctx, next) { features: { userinfo, mTLS: { getCertificate }, + dPoP: { allowReplay }, resourceIndicators, }, } = conf; @@ -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, diff --git a/lib/actions/userinfo.js b/lib/actions/userinfo.js index 31919bda9..965e488e1 100644 --- a/lib/actions/userinfo.js +++ b/lib/actions/userinfo.js @@ -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)) { diff --git a/lib/helpers/defaults.js b/lib/helpers/defaults.js index f380a58a2..543a8c6a2 100644 --- a/lib/helpers/defaults.js +++ b/lib/helpers/defaults.js @@ -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, }, /*