You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by co...@apache.org on 2026/07/20 08:19:39 UTC

(cxf) branch 4.1.x-fixes updated (8a3f3504b4e -> 8146082be06)

This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a change to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git


    from 8a3f3504b4e Recording .gitmergeinfo Changes
     new d130b732b4d Enforce TLS client certificate and store the application certificates for dynamic registration (#3316)
     new 8146082be06 Don't perpetuate security sensitive values from the JWT (#3317)

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../oauth2/grants/code/JwtRequestCodeFilter.java   |  19 ++
 .../oauth2/services/AbstractTokenService.java      |  37 ++--
 .../services/DynamicRegistrationService.java       |  59 ++++++
 .../grants/code/JwtRequestCodeFilterTest.java      | 201 +++++++++++++++++++++
 .../oauth2/services/AbstractTokenServiceTest.java  |  90 +++++++++
 .../services/DynamicRegistrationServiceTest.java   | 102 +++++++++++
 .../cxf/systest/jaxrs/security/oidc/client.xml     |   3 +
 .../jaxrs/security/oidc/oidc-server-dynreg.xml     |   2 +-
 8 files changed, 495 insertions(+), 18 deletions(-)
 create mode 100644 rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/grants/code/JwtRequestCodeFilterTest.java


(cxf) 01/02: Enforce TLS client certificate and store the application certificates for dynamic registration (#3316)

Posted by co...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit d130b732b4d4af28a26f47840f7272caf402b22a
Author: Colm O hEigeartaigh <co...@users.noreply.github.com>
AuthorDate: Mon Jul 20 09:11:06 2026 +0100

    Enforce TLS client certificate and store the application certificates for dynamic registration (#3316)
    
    (cherry picked from commit 413d75ec1d9bd1b6a2b50e72bf4e7f0e53258218)
---
 .../oauth2/services/AbstractTokenService.java      |  37 ++++----
 .../services/DynamicRegistrationService.java       |  59 ++++++++++++
 .../oauth2/services/AbstractTokenServiceTest.java  |  90 ++++++++++++++++++
 .../services/DynamicRegistrationServiceTest.java   | 102 +++++++++++++++++++++
 .../cxf/systest/jaxrs/security/oidc/client.xml     |   3 +
 .../jaxrs/security/oidc/oidc-server-dynreg.xml     |   2 +-
 6 files changed, 275 insertions(+), 18 deletions(-)

diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenService.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenService.java
index 0f42687bb85..4b90ffe4609 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenService.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenService.java
@@ -161,26 +161,29 @@ public class AbstractTokenService extends AbstractOAuthService {
     }
 
     protected void checkCertificateBinding(Client client, TLSSessionInfo tlsSessionInfo) {
-        String subjectDn = client.getProperties().get(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN);
-        if (subjectDn == null && client.getApplicationCertificates().isEmpty()) {
-            LOG.warning("Client \"" + client.getClientId() + "\" can not be bound to the TLS certificate");
-            reportInvalidClient();
-        }
         X509Certificate cert = OAuthUtils.getRootTLSCertificate(tlsSessionInfo);
-
-        if (subjectDn != null
-            && !subjectDn.equals(OAuthUtils.getSubjectDnFromTLSCertificates(cert))) {
-            LOG.warning("Client \"" + client.getClientId() + "\" can not be bound to the TLS certificate");
-            reportInvalidClient();
-        }
-        String issuerDn = client.getProperties().get(OAuthConstants.TLS_CLIENT_AUTH_ISSUER_DN);
-        if (issuerDn != null
-            && !issuerDn.equals(OAuthUtils.getIssuerDnFromTLSCertificates(cert))) {
-            LOG.warning("Client \"" + client.getClientId() + "\" can not be bound to the TLS certificate");
-            reportInvalidClient();
-        }
         if (!client.getApplicationCertificates().isEmpty()) {
             compareTlsCertificates(tlsSessionInfo, client.getApplicationCertificates());
+        } else {
+            String subjectDn = client.getProperties().get(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN);
+            String issuerDn = client.getProperties().get(OAuthConstants.TLS_CLIENT_AUTH_ISSUER_DN);
+            if (subjectDn == null && issuerDn == null) {
+                LOG.warning("Client \"" + client.getClientId()
+                    + "\" can not be bound to the TLS certificate");
+                reportInvalidClient();
+            }
+            if (subjectDn != null
+                && !subjectDn.equals(OAuthUtils.getSubjectDnFromTLSCertificates(cert))) {
+                LOG.warning("Client \"" + client.getClientId()
+                    + "\" can not be bound to the TLS certificate");
+                reportInvalidClient();
+            }
+            if (issuerDn != null
+                && !issuerDn.equals(OAuthUtils.getIssuerDnFromTLSCertificates(cert))) {
+                LOG.warning("Client \"" + client.getClientId()
+                    + "\" can not be bound to the TLS certificate");
+                reportInvalidClient();
+            }
         }
         OAuthUtils.setCertificateThumbprintConfirmation(getMessageContext(), cert);
     }
diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/DynamicRegistrationService.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/DynamicRegistrationService.java
index eb0e387a744..8be6b6d9bda 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/DynamicRegistrationService.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/services/DynamicRegistrationService.java
@@ -18,10 +18,13 @@
  */
 package org.apache.cxf.rs.security.oauth2.services;
 
+import java.security.cert.X509Certificate;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 
+import javax.security.auth.x500.X500Principal;
+
 import jakarta.ws.rs.Consumes;
 import jakarta.ws.rs.DELETE;
 import jakarta.ws.rs.GET;
@@ -38,6 +41,7 @@ import jakarta.ws.rs.core.Response.ResponseBuilder;
 import jakarta.ws.rs.core.SecurityContext;
 import jakarta.ws.rs.core.UriBuilder;
 import org.apache.cxf.common.util.Base64UrlUtility;
+import org.apache.cxf.common.util.Base64Utility;
 import org.apache.cxf.common.util.StringUtils;
 import org.apache.cxf.jaxrs.ext.MessageContext;
 import org.apache.cxf.jaxrs.utils.ExceptionUtils;
@@ -51,6 +55,7 @@ import org.apache.cxf.rs.security.oauth2.utils.AuthorizationUtils;
 import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
 import org.apache.cxf.rs.security.oauth2.utils.OAuthUtils;
 import org.apache.cxf.rt.security.crypto.CryptoUtils;
+import org.apache.cxf.security.transport.TLSSessionInfo;
 
 @Path("register")
 public class DynamicRegistrationService {
@@ -62,6 +67,7 @@ public class DynamicRegistrationService {
     private int clientIdSizeInBytes = DEFAULT_CLIENT_ID_SIZE;
     private MessageContext mc;
     private boolean supportRegistrationAccessTokens = true;
+    private boolean enforceTlsClientAuthCertificateBinding = true;
     private String userRole;
     private List<String> allowedClientScopes;
 
@@ -274,6 +280,8 @@ public class DynamicRegistrationService {
 
         newClient.setTokenEndpointAuthMethod(tokenEndpointAuthMethod);
         if (OAuthConstants.TOKEN_ENDPOINT_AUTH_TLS.equals(tokenEndpointAuthMethod)) {
+            X509Certificate cert = enforceTlsClientAuthCertificateBinding
+                ? validateTlsClientAuthCertificateBinding(request) : getTlsClientCertificate();
             String subjectDn = (String)request.getProperty(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN);
             if (subjectDn != null) {
                 newClient.getProperties().put(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN, subjectDn);
@@ -282,6 +290,15 @@ public class DynamicRegistrationService {
             if (issuerDn != null) {
                 newClient.getProperties().put(OAuthConstants.TLS_CLIENT_AUTH_ISSUER_DN, issuerDn);
             }
+            if (cert != null) {
+                try {
+                    newClient.getApplicationCertificates().add(Base64Utility.encode(cert.getEncoded()));
+                } catch (Exception ex) {
+                    OAuthError error =
+                        new OAuthError(INVALID_CLIENT_METADATA, "Unable to register TLS client certificate");
+                    reportInvalidRequestError(error);
+                }
+            }
         }
         // Client Registration Time
         newClient.setRegisteredAt(System.currentTimeMillis() / 1000L);
@@ -298,6 +315,44 @@ public class DynamicRegistrationService {
         return newClient;
     }
 
+    protected X509Certificate getTlsClientCertificate() {
+        TLSSessionInfo tlsSessionInfo = (TLSSessionInfo)mc.get(TLSSessionInfo.class.getName());
+        return tlsSessionInfo == null ? null : OAuthUtils.getRootTLSCertificate(tlsSessionInfo);
+    }
+
+    protected X509Certificate validateTlsClientAuthCertificateBinding(ClientRegistration request) {
+        X509Certificate cert = getTlsClientCertificate();
+        if (cert == null) {
+            OAuthError error =
+                new OAuthError(INVALID_CLIENT_METADATA, "TLS client certificate is required");
+            reportInvalidRequestError(error);
+        }
+
+        String subjectDn = (String)request.getProperty(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN);
+        if (subjectDn != null
+            && !isSameDistinguishedName(subjectDn, OAuthUtils.getSubjectDnFromTLSCertificates(cert))) {
+            OAuthError error =
+                new OAuthError(INVALID_CLIENT_METADATA, "Invalid tls_client_auth_subject_dn metadata");
+            reportInvalidRequestError(error);
+        }
+        String issuerDn = (String)request.getProperty(OAuthConstants.TLS_CLIENT_AUTH_ISSUER_DN);
+        if (issuerDn != null
+            && !isSameDistinguishedName(issuerDn, OAuthUtils.getIssuerDnFromTLSCertificates(cert))) {
+            OAuthError error =
+                new OAuthError(INVALID_CLIENT_METADATA, "Invalid tls_client_auth_root_dn metadata");
+            reportInvalidRequestError(error);
+        }
+        return cert;
+    }
+
+    private boolean isSameDistinguishedName(String expectedDn, String actualDn) {
+        try {
+            return new X500Principal(expectedDn).equals(new X500Principal(actualDn));
+        } catch (IllegalArgumentException ex) {
+            return false;
+        }
+    }
+
     protected void fromClientRegistrationToClient(ClientRegistration request, Client client) {
         final List<String> grantTypes = client.getAllowedGrantTypes();
 
@@ -419,6 +474,10 @@ public class DynamicRegistrationService {
         this.supportRegistrationAccessTokens = supportRegistrationAccessTokens;
     }
 
+    public void setEnforceTlsClientAuthCertificateBinding(boolean enforceTlsClientAuthCertificateBinding) {
+        this.enforceTlsClientAuthCertificateBinding = enforceTlsClientAuthCertificateBinding;
+    }
+
     public void setUserRole(String userRole) {
         this.userRole = userRole;
     }
diff --git a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenServiceTest.java b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenServiceTest.java
index 4e56b366987..91bba44fb2b 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenServiceTest.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/services/AbstractTokenServiceTest.java
@@ -18,12 +18,21 @@
  */
 package org.apache.cxf.rs.security.oauth2.services;
 
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+
+import javax.security.auth.x500.X500Principal;
+
 import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.core.SecurityContext;
+import org.apache.cxf.common.util.Base64Utility;
+import org.apache.cxf.jaxrs.ext.MessageContext;
 import org.apache.cxf.jaxrs.impl.MetadataMap;
 import org.apache.cxf.rs.security.oauth2.common.Client;
 import org.apache.cxf.rs.security.oauth2.common.OAuthError;
 import org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException;
 import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
+import org.apache.cxf.security.transport.TLSSessionInfo;
 
 import org.junit.Test;
 
@@ -31,6 +40,8 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 public class AbstractTokenServiceTest {
 
@@ -79,6 +90,76 @@ public class AbstractTokenServiceTest {
         assertSame(expected, actual);
     }
 
+    @Test
+    public void testCheckCertificateBindingAcceptsDnOnlyClient() {
+        TestTokenService service = new TestTokenService();
+        Client client = new Client("c1", null, true);
+        client.getProperties().put(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN, "CN=peer,O=Test,C=US");
+
+        service.callCheckCertificateBinding(client,
+            makeTlsSession("CN=peer,O=Test,C=US", "CN=issuer,O=Test,C=US"));
+    }
+
+    @Test
+    public void testCheckCertificateBindingRejectsDnOnlyClientMismatch() {
+        TestTokenService service = new TestTokenService();
+        Client client = new Client("c1", null, true);
+        client.getProperties().put(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN, "CN=peer,O=Test,C=US");
+
+        InvalidClientException ex = assertThrows(InvalidClientException.class,
+            () -> service.callCheckCertificateBinding(client,
+                makeTlsSession("CN=other,O=Test,C=US", "CN=issuer,O=Test,C=US")));
+
+        assertNotNull(ex.getError());
+        assertEquals(OAuthConstants.INVALID_CLIENT, ex.getError().getError());
+    }
+
+    @Test
+    public void testCheckCertificateBindingRejectsCertificateMismatch() {
+        TestTokenService service = new TestTokenService();
+        Client client = new Client("c2", null, true);
+        client.getApplicationCertificates().add(Base64Utility.encode(new byte[] {9, 9, 9}));
+
+        InvalidClientException ex = assertThrows(InvalidClientException.class,
+            () -> service.callCheckCertificateBinding(client,
+                makeTlsSession("CN=peer,O=Test,C=US", "CN=issuer,O=Test,C=US")));
+
+        assertNotNull(ex.getError());
+        assertEquals(OAuthConstants.INVALID_CLIENT, ex.getError().getError());
+    }
+
+    @Test
+    public void testCheckCertificateBindingAcceptsCertificateMatch() throws Exception {
+        TestTokenService service = new TestTokenService();
+        X509Certificate cert = createCertificate("CN=peer,O=Test,C=US", "CN=issuer,O=Test,C=US");
+
+        Client client = new Client("c3", null, true);
+        client.getApplicationCertificates().add(Base64Utility.encode(cert.getEncoded()));
+
+        service.callCheckCertificateBinding(client, makeTlsSession(cert));
+    }
+
+    private static TLSSessionInfo makeTlsSession(String subjectDn, String issuerDn) {
+        X509Certificate cert = createCertificate(subjectDn, issuerDn);
+        return makeTlsSession(cert);
+    }
+
+    private static TLSSessionInfo makeTlsSession(X509Certificate cert) {
+        return new TLSSessionInfo("TLS_FAKE", null, new Certificate[] {cert});
+    }
+
+    private static X509Certificate createCertificate(String subjectDn, String issuerDn) {
+        X509Certificate cert = mock(X509Certificate.class);
+        when(cert.getSubjectX500Principal()).thenReturn(new X500Principal(subjectDn));
+        when(cert.getIssuerX500Principal()).thenReturn(new X500Principal(issuerDn));
+        try {
+            when(cert.getEncoded()).thenReturn(new byte[] {1, 2, 3});
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+        return cert;
+    }
+
     private static final class TestTokenService extends AbstractTokenService {
         private Client client;
         private OAuthServiceException exception;
@@ -87,6 +168,15 @@ public class AbstractTokenServiceTest {
             return getClient(clientId, clientSecret, params);
         }
 
+        void callCheckCertificateBinding(Client boundClient, TLSSessionInfo tlsSessionInfo) {
+            MessageContext mc = mock(MessageContext.class);
+            SecurityContext sc = mock(SecurityContext.class);
+            when(sc.getAuthenticationScheme()).thenReturn("");
+            when(mc.getSecurityContext()).thenReturn(sc);
+            setMessageContext(mc);
+            checkCertificateBinding(boundClient, tlsSessionInfo);
+        }
+
         void setClient(Client client) {
             this.client = client;
         }
diff --git a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/services/DynamicRegistrationServiceTest.java b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/services/DynamicRegistrationServiceTest.java
index 72fddf6516a..fa339c5b11c 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/services/DynamicRegistrationServiceTest.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/services/DynamicRegistrationServiceTest.java
@@ -18,19 +18,28 @@
  */
 package org.apache.cxf.rs.security.oauth2.services;
 
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.Collections;
 
+import javax.security.auth.x500.X500Principal;
+
 import jakarta.ws.rs.BadRequestException;
+import jakarta.ws.rs.core.SecurityContext;
+import org.apache.cxf.jaxrs.ext.MessageContext;
 import org.apache.cxf.rs.security.oauth2.common.Client;
 import org.apache.cxf.rs.security.oauth2.common.OAuthError;
 import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
+import org.apache.cxf.security.transport.TLSSessionInfo;
 
 import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 public class DynamicRegistrationServiceTest {
 
@@ -80,6 +89,95 @@ public class DynamicRegistrationServiceTest {
         assertEquals(Collections.singletonList("openid"), client.getRegisteredScopes());
     }
 
+    @Test
+    public void testRejectsTlsClientAuthWithoutTlsCertificate() {
+        TestDynamicRegistrationService service = new TestDynamicRegistrationService();
+        service.setEnforceTlsClientAuthCertificateBinding(true);
+        service.setMessageContext(createMessageContext("", null));
+
+        ClientRegistration request = new ClientRegistration();
+        request.setGrantTypes(Collections.singletonList(OAuthConstants.CLIENT_CREDENTIALS_GRANT));
+        request.setTokenEndpointAuthMethod(OAuthConstants.TOKEN_ENDPOINT_AUTH_TLS);
+        request.setProperty(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN,
+            "CN=client,OU=Test,O=Apache,C=US");
+
+        BadRequestException ex = assertThrows(BadRequestException.class,
+            () -> service.createClient(request));
+
+        assertInvalidClientMetadata(ex);
+    }
+
+    @Test
+    public void testRejectsTlsClientAuthWhenSubjectDnDoesNotMatchCertificate() {
+        TestDynamicRegistrationService service = new TestDynamicRegistrationService();
+        service.setEnforceTlsClientAuthCertificateBinding(true);
+        X509Certificate cert = createCertificate(
+            "CN=actual,OU=Test,O=Apache,C=US",
+            "CN=issuer,OU=Test,O=Apache,C=US");
+        service.setMessageContext(createMessageContext("", cert));
+
+        ClientRegistration request = new ClientRegistration();
+        request.setGrantTypes(Collections.singletonList(OAuthConstants.CLIENT_CREDENTIALS_GRANT));
+        request.setTokenEndpointAuthMethod(OAuthConstants.TOKEN_ENDPOINT_AUTH_TLS);
+        request.setProperty(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN,
+            "CN=expected,OU=Test,O=Apache,C=US");
+
+        BadRequestException ex = assertThrows(BadRequestException.class,
+            () -> service.createClient(request));
+
+        assertInvalidClientMetadata(ex);
+    }
+
+    @Test
+    public void testAcceptsTlsClientAuthWhenSubjectDnMatchesCertificate() {
+        TestDynamicRegistrationService service = new TestDynamicRegistrationService();
+        service.setEnforceTlsClientAuthCertificateBinding(true);
+        String subjectDn = "CN=client,OU=Test,O=Apache,C=US";
+        X509Certificate cert = createCertificate(subjectDn, "CN=issuer,OU=Test,O=Apache,C=US");
+        service.setMessageContext(createMessageContext("", cert));
+
+        ClientRegistration request = new ClientRegistration();
+        request.setGrantTypes(Collections.singletonList(OAuthConstants.CLIENT_CREDENTIALS_GRANT));
+        request.setTokenEndpointAuthMethod(OAuthConstants.TOKEN_ENDPOINT_AUTH_TLS);
+        request.setProperty(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN, subjectDn);
+
+        Client client = service.createClient(request);
+        assertEquals(subjectDn, client.getProperties().get(OAuthConstants.TLS_CLIENT_AUTH_SUBJECT_DN));
+        assertEquals(1, client.getApplicationCertificates().size());
+    }
+
+    private static void assertInvalidClientMetadata(BadRequestException ex) {
+        assertNotNull(ex.getResponse());
+        OAuthError error = (OAuthError)ex.getResponse().getEntity();
+        assertNotNull(error);
+        assertEquals("invalid_client_metadata", error.getError());
+    }
+
+    private static MessageContext createMessageContext(String authScheme, X509Certificate cert) {
+        SecurityContext sc = mock(SecurityContext.class);
+        when(sc.getAuthenticationScheme()).thenReturn(authScheme);
+
+        MessageContext mc = mock(MessageContext.class);
+        when(mc.getSecurityContext()).thenReturn(sc);
+        if (cert != null) {
+            TLSSessionInfo tlsInfo = new TLSSessionInfo("TLS_FAKE", null, new Certificate[] {cert});
+            when(mc.get(TLSSessionInfo.class.getName())).thenReturn(tlsInfo);
+        }
+        return mc;
+    }
+
+    private static X509Certificate createCertificate(String subjectDn, String issuerDn) {
+        X509Certificate cert = mock(X509Certificate.class);
+        when(cert.getSubjectX500Principal()).thenReturn(new X500Principal(subjectDn));
+        when(cert.getIssuerX500Principal()).thenReturn(new X500Principal(issuerDn));
+        try {
+            when(cert.getEncoded()).thenReturn(new byte[] {1, 2, 3});
+        } catch (Exception ex) {
+            throw new RuntimeException(ex);
+        }
+        return cert;
+    }
+
     private static Client createClient() {
         Client client = new Client("client", "secret", true);
         client.setAllowedGrantTypes(Collections.singletonList(OAuthConstants.CLIENT_CREDENTIALS_GRANT));
@@ -90,5 +188,9 @@ public class DynamicRegistrationServiceTest {
         void applyClientRegistration(ClientRegistration request, Client client) {
             fromClientRegistrationToClient(request, client);
         }
+
+        Client createClient(ClientRegistration request) {
+            return createNewClient(request);
+        }
     }
 }
diff --git a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oidc/client.xml b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oidc/client.xml
index b90f73d9835..a0fdf5f7977 100644
--- a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oidc/client.xml
+++ b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oidc/client.xml
@@ -36,6 +36,9 @@
     <http:conduit name="https://localhost.*">
         <http:client ConnectionTimeout="3000000" ReceiveTimeout="3000000"/>
         <http:tlsClientParameters disableCNCheck="true">
+            <sec:keyManagers keyPassword="password">
+                <sec:keyStore type="JKS" password="password" resource="keys/Morpit.jks"/>
+            </sec:keyManagers>
             <sec:trustManagers>
                 <sec:keyStore type="JKS" password="password" resource="keys/Truststore.jks"/>
             </sec:trustManagers>
diff --git a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oidc/oidc-server-dynreg.xml b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oidc/oidc-server-dynreg.xml
index bc4c6b47d82..a9fd1cbc8b8 100644
--- a/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oidc/oidc-server-dynreg.xml
+++ b/systests/rs-security/src/test/resources/org/apache/cxf/systest/jaxrs/security/oidc/oidc-server-dynreg.xml
@@ -56,7 +56,7 @@ under the License.
                 <sec:trustManagers>
                     <sec:keyStore type="JKS" password="password" resource="keys/Truststore.jks"/>
                 </sec:trustManagers>
-                <sec:clientAuthentication want="false" required="false"/>
+                <sec:clientAuthentication want="true" required="false"/>
             </httpj:tlsServerParameters>
             <httpj:sessionSupport>true</httpj:sessionSupport>
         </httpj:engine>


(cxf) 02/02: Don't perpetuate security sensitive values from the JWT (#3317)

Posted by co...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

coheigea pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit 8146082be06fd509a1ff385190a0744622549f1c
Author: Colm O hEigeartaigh <co...@users.noreply.github.com>
AuthorDate: Mon Jul 20 09:12:00 2026 +0100

    Don't perpetuate security sensitive values from the JWT (#3317)
    
    (cherry picked from commit fd3cc5f7d936f5f3fc796a402e10069e0d1200a4)
---
 .../oauth2/grants/code/JwtRequestCodeFilter.java   |  19 ++
 .../grants/code/JwtRequestCodeFilterTest.java      | 201 +++++++++++++++++++++
 2 files changed, 220 insertions(+)

diff --git a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/code/JwtRequestCodeFilter.java b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/code/JwtRequestCodeFilter.java
index 285d1b00ff5..d0681f9017f 100644
--- a/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/code/JwtRequestCodeFilter.java
+++ b/rt/rs/security/oauth-parent/oauth2/src/main/java/org/apache/cxf/rs/security/oauth2/grants/code/JwtRequestCodeFilter.java
@@ -19,8 +19,11 @@
 package org.apache.cxf.rs.security.oauth2.grants.code;
 
 import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.logging.Logger;
 
 import jakarta.ws.rs.core.MultivaluedMap;
@@ -47,6 +50,17 @@ public class JwtRequestCodeFilter extends OAuthJoseJwtConsumer implements Author
     private static final String REQUEST_URI_CONTENT_TYPE = "application/oauth-authz-req+jwt";
     private static final String REQUEST_PARAM = "request";
     private static final String REQUEST_URI_PARAM = "request_uri";
+    /**
+     * Security-sensitive parameters that must be sourced from the outer HTTP
+     * request per RFC 9101 §2.2.  Any JWT claim whose key appears in this set
+     * is silently ignored; the outer request value is always authoritative.
+     */
+    private static final Set<String> OUTER_REQUEST_ONLY_PARAMS = new HashSet<>(Arrays.asList(
+            OAuthConstants.AUTHORIZATION_CODE_CHALLENGE,
+            OAuthConstants.AUTHORIZATION_CODE_CHALLENGE_METHOD,
+            OAuthConstants.NONCE,
+            OAuthConstants.STATE
+    ));
 
     private boolean verifyWithClientCertificates;
     private String issuer;
@@ -105,6 +119,11 @@ public class JwtRequestCodeFilter extends OAuthJoseJwtConsumer implements Author
                     List<Object> list = CastUtils.cast((List<?>)value);
                     value = jsonHandler.toJson(list);
                 }
+                // RFC 9101 §2.2: security-sensitive parameters must be sourced
+                // from the outer request; JWT claims for these keys are ignored.
+                if (OUTER_REQUEST_ONLY_PARAMS.contains(key)) {
+                    continue;
+                }
                 newParams.putSingle(key, value.toString());
             }
             return newParams;
diff --git a/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/grants/code/JwtRequestCodeFilterTest.java b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/grants/code/JwtRequestCodeFilterTest.java
new file mode 100644
index 00000000000..ac4792cece1
--- /dev/null
+++ b/rt/rs/security/oauth-parent/oauth2/src/test/java/org/apache/cxf/rs/security/oauth2/grants/code/JwtRequestCodeFilterTest.java
@@ -0,0 +1,201 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cxf.rs.security.oauth2.grants.code;
+
+import jakarta.ws.rs.core.MultivaluedMap;
+import org.apache.cxf.jaxrs.impl.MetadataMap;
+import org.apache.cxf.rs.security.jose.jwa.SignatureAlgorithm;
+import org.apache.cxf.rs.security.jose.jws.HmacJwsSignatureProvider;
+import org.apache.cxf.rs.security.jose.jws.HmacJwsSignatureVerifier;
+import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactProducer;
+import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
+import org.apache.cxf.rs.security.oauth2.common.Client;
+import org.apache.cxf.rs.security.oauth2.utils.OAuthConstants;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests that security-sensitive outer request parameters (code_challenge,
+ * code_challenge_method, nonce, state) cannot be silently overridden by
+ * claims inside a JAR (JWT Authorization Request) — even when the JWT
+ * carries a valid signature — per RFC 9101 §2.2.
+ */
+public class JwtRequestCodeFilterTest {
+
+    private static final String CLIENT_ID = "test-client";
+    /**
+     * Signing key used for HMAC-SHA256; length ≥ 32 bytes satisfies HS256
+     * security requirements for unit-test purposes.
+     */
+    private static final String SIGNING_KEY = "jwt-request-code-filter-test-key";
+
+    // ------------------------------------------------------------------
+    // Helpers
+    // ------------------------------------------------------------------
+
+    private static JwtRequestCodeFilter buildFilter() {
+        JwtRequestCodeFilter filter = new JwtRequestCodeFilter();
+        // Inject a verifier directly so the filter can validate the JWT
+        // without loading key material from system properties or a keystore.
+        filter.setJwsVerifier(new HmacJwsSignatureVerifier(SIGNING_KEY, SignatureAlgorithm.HS256));
+        return filter;
+    }
+
+    private static Client buildClient() {
+        return new Client(CLIENT_ID, "secret", true);
+    }
+
+    /** Returns baseline JWT claims that satisfy the filter's own issuer check. */
+    private static JwtClaims baselineClaims() {
+        JwtClaims claims = new JwtClaims();
+        claims.setIssuer(CLIENT_ID);
+        return claims;
+    }
+
+    private static String buildSignedJwt(JwtClaims claims) {
+        JwsJwtCompactProducer producer = new JwsJwtCompactProducer(claims);
+        return producer.signWith(
+                new HmacJwsSignatureProvider(SIGNING_KEY, SignatureAlgorithm.HS256));
+    }
+
+    // ------------------------------------------------------------------
+    // Vulnerability tests (currently FAIL — demonstrate CWE-20 / RFC 9101)
+    // ------------------------------------------------------------------
+
+    /**
+     * A validly-signed request JWT carrying {@code code_challenge} and
+     * {@code code_challenge_method} must not replace the values supplied
+     * in the outer HTTP request.  Allowing the override lets an attacker
+     * who controls the JWT payload substitute the client's PKCE challenge
+     * with one of their choosing (CVSS 3.1 ~4.1 MEDIUM).
+     */
+    @Test
+    public void testCodeChallengeNotOverriddenByJwtClaim() {
+        JwtRequestCodeFilter filter = buildFilter();
+
+        JwtClaims claims = baselineClaims();
+        claims.setClaim(OAuthConstants.AUTHORIZATION_CODE_CHALLENGE, "EVIL-CHALLENGE");
+        claims.setClaim(OAuthConstants.AUTHORIZATION_CODE_CHALLENGE_METHOD, "S256");
+
+        MultivaluedMap<String, String> params = new MetadataMap<>();
+        params.putSingle(OAuthConstants.RESPONSE_TYPE, OAuthConstants.CODE_RESPONSE_TYPE);
+        params.putSingle(OAuthConstants.AUTHORIZATION_CODE_CHALLENGE, "legit-client-challenge");
+        params.putSingle(OAuthConstants.AUTHORIZATION_CODE_CHALLENGE_METHOD, "plain");
+        params.putSingle("request", buildSignedJwt(claims));
+
+        MultivaluedMap<String, String> result = filter.process(params, null, buildClient());
+
+        assertEquals("JWT must not override the outer code_challenge",
+                "legit-client-challenge",
+                result.getFirst(OAuthConstants.AUTHORIZATION_CODE_CHALLENGE));
+        assertEquals("JWT must not override the outer code_challenge_method",
+                "plain",
+                result.getFirst(OAuthConstants.AUTHORIZATION_CODE_CHALLENGE_METHOD));
+    }
+
+    /**
+     * {@code nonce} is an anti-replay parameter established by the client
+     * in the outer request.  A JWT must not be allowed to replace it, as
+     * doing so undermines OpenID Connect replay protection.
+     */
+    @Test
+    public void testNonceNotOverriddenByJwtClaim() {
+        JwtRequestCodeFilter filter = buildFilter();
+
+        JwtClaims claims = baselineClaims();
+        claims.setClaim(OAuthConstants.NONCE, "EVIL-NONCE");
+
+        MultivaluedMap<String, String> params = new MetadataMap<>();
+        params.putSingle(OAuthConstants.RESPONSE_TYPE, OAuthConstants.CODE_RESPONSE_TYPE);
+        params.putSingle(OAuthConstants.NONCE, "outer-nonce");
+        params.putSingle("request", buildSignedJwt(claims));
+
+        MultivaluedMap<String, String> result = filter.process(params, null, buildClient());
+
+        assertEquals("JWT must not override the outer nonce",
+                "outer-nonce",
+                result.getFirst(OAuthConstants.NONCE));
+    }
+
+    /**
+     * {@code state} is the client's CSRF token.  A JWT must not be allowed
+     * to replace it, as doing so can redirect the authorization response
+     * to an attacker-controlled state.
+     */
+    @Test
+    public void testStateNotOverriddenByJwtClaim() {
+        JwtRequestCodeFilter filter = buildFilter();
+
+        JwtClaims claims = baselineClaims();
+        claims.setClaim(OAuthConstants.STATE, "EVIL-STATE");
+
+        MultivaluedMap<String, String> params = new MetadataMap<>();
+        params.putSingle(OAuthConstants.RESPONSE_TYPE, OAuthConstants.CODE_RESPONSE_TYPE);
+        params.putSingle(OAuthConstants.STATE, "outer-state");
+        params.putSingle("request", buildSignedJwt(claims));
+
+        MultivaluedMap<String, String> result = filter.process(params, null, buildClient());
+
+        assertEquals("JWT must not override the outer state",
+                "outer-state",
+                result.getFirst(OAuthConstants.STATE));
+    }
+
+    /**
+     * Non-sensitive claims present in the JWT (e.g. {@code scope}) that are
+     * absent from the outer request should still be applied to the resulting
+     * parameter map — the fix must not strip all JWT claims.
+     */
+    @Test
+    public void testNonSensitiveJwtClaimsAreApplied() {
+        JwtRequestCodeFilter filter = buildFilter();
+
+        JwtClaims claims = baselineClaims();
+        claims.setClaim(OAuthConstants.SCOPE, "read write");
+
+        MultivaluedMap<String, String> params = new MetadataMap<>();
+        params.putSingle(OAuthConstants.RESPONSE_TYPE, OAuthConstants.CODE_RESPONSE_TYPE);
+        params.putSingle("request", buildSignedJwt(claims));
+
+        MultivaluedMap<String, String> result = filter.process(params, null, buildClient());
+
+        assertEquals("Scope from JWT should be applied when absent from outer request",
+                "read write",
+                result.getFirst(OAuthConstants.SCOPE));
+    }
+
+    /**
+     * When no {@code request} parameter is present the filter must return
+     * the original params unchanged.
+     */
+    @Test
+    public void testNoRequestParamReturnsOriginalParams() {
+        JwtRequestCodeFilter filter = buildFilter();
+
+        MultivaluedMap<String, String> params = new MetadataMap<>();
+        params.putSingle(OAuthConstants.RESPONSE_TYPE, OAuthConstants.CODE_RESPONSE_TYPE);
+        params.putSingle(OAuthConstants.STATE, "original-state");
+
+        MultivaluedMap<String, String> result = filter.process(params, null, buildClient());
+
+        assertEquals("original-state", result.getFirst(OAuthConstants.STATE));
+    }
+}