From d03a08fad739866d409cc621ad86fa63a51aa53d Mon Sep 17 00:00:00 2001 From: Thomas Diesler Date: Mon, 27 Apr 2026 15:47:43 +0200 Subject: [PATCH] [OID4VCI-FAPI2] Add DPoP as alternative holder of key proof closes #48502 Signed-off-by: Thomas Diesler --- .../clientpolicy/executor/HolderOfKeyEnforcerExecutor.java | 7 +++++-- .../test/java/org/keycloak/testsuite/client/CIBATest.java | 2 +- .../test/java/org/keycloak/testsuite/client/FAPI1Test.java | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/HolderOfKeyEnforcerExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/HolderOfKeyEnforcerExecutor.java index ca36bcc949f4..f6d8b8369c04 100644 --- a/services/src/main/java/org/keycloak/services/clientpolicy/executor/HolderOfKeyEnforcerExecutor.java +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/HolderOfKeyEnforcerExecutor.java @@ -28,6 +28,7 @@ import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.representations.AccessToken; import org.keycloak.representations.RefreshToken; +import org.keycloak.representations.dpop.DPoP; import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.services.clientpolicy.ClientPolicyContext; @@ -37,6 +38,7 @@ import org.keycloak.services.clientpolicy.context.TokenRefreshContext; import org.keycloak.services.clientpolicy.context.TokenRevokeContext; import org.keycloak.services.clientpolicy.context.UserInfoRequestContext; +import org.keycloak.services.util.DPoPUtil; import org.keycloak.services.util.MtlsHoKTokenUtil; import com.fasterxml.jackson.annotation.JsonProperty; @@ -91,9 +93,10 @@ public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyExcep case TOKEN_REQUEST: case SERVICE_ACCOUNT_TOKEN_REQUEST: case BACKCHANNEL_TOKEN_REQUEST: + DPoP dpop = session.getAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, DPoP.class); AccessToken.Confirmation certConf = MtlsHoKTokenUtil.bindTokenWithClientCertificate(request, session); - if (certConf == null) { - throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Client Certification missing for MTLS HoK Token Binding"); + if (dpop == null && certConf == null) { + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Holder of Key Proof required (e.g. mTLS, DPoP)"); } break; case TOKEN_REFRESH: diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java index 87634f48c2e2..8e72ebbde64a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/CIBATest.java @@ -2267,7 +2267,7 @@ public void testHolderOfKeyEnforceExecutor() throws Exception { AccessTokenResponse tokenRes = oauth.ciba().doBackchannelAuthenticationTokenRequest(response.getAuthReqId()); assertThat(tokenRes.getStatusCode(), is(equalTo(400))); assertThat(tokenRes.getError(), is(equalTo(OAuthErrorException.INVALID_GRANT))); - assertThat(tokenRes.getErrorDescription(), is(equalTo("Client Certification missing for MTLS HoK Token Binding"))); + assertThat(tokenRes.getErrorDescription(), is(equalTo("Holder of Key Proof required (e.g. mTLS, DPoP)"))); events.expect(EventType.AUTHREQID_TO_TOKEN_ERROR).clearDetails().user((String)null).client(TEST_CLIENT_NAME).error(OAuthErrorException.INVALID_REQUEST).assertEvent(); oauth.httpClient().reset(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/FAPI1Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/FAPI1Test.java index 6e2e8d389456..c8f5819e156f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/FAPI1Test.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/FAPI1Test.java @@ -466,7 +466,7 @@ public void testFAPIAdvancedLoginWithPrivateKeyJWT() throws Exception { String signedJwt = createSignedRequestToken("foo", privateKey, publicKey, org.keycloak.crypto.Algorithm.PS256); AccessTokenResponse tokenResponse = doAccessTokenRequestWithClientSignedJWT(code, signedJwt, DefaultHttpClient::new); Assertions.assertEquals(OAuthErrorException.INVALID_GRANT,tokenResponse.getError()); - Assertions.assertEquals("Client Certification missing for MTLS HoK Token Binding", tokenResponse.getErrorDescription()); + Assertions.assertEquals("Holder of Key Proof required (e.g. mTLS, DPoP)", tokenResponse.getErrorDescription()); // Login with private-key-jwt client authentication and MTLS added to HttpClient. TokenRequest should be successful now oauth.loginForm().requestUri(requestUri).open();