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

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

commit 08d5bcf35e68b63555ea9996109851b8c86a8801
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Tue Aug 25 11:35:05 2026 +0100

    For OIDC, allow only relative URLs or absolute URLs with the same scheme 
and authority as the current RP
---
 .../oidc/rp/OidcRpAuthenticationFilter.java        |   5 +-
 .../oidc/rp/OidcRpAuthenticationService.java       |  16 ++-
 .../oidc/rp/OidcRpAuthenticationUtils.java         |  36 +++++++
 .../oidc/rp/OidcRpAuthenticationServiceTest.java   | 120 +++++++++++++++++++++
 4 files changed, 172 insertions(+), 5 deletions(-)

diff --git 
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationFilter.java
 
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationFilter.java
index b08d907fe64..e835252f81b 100644
--- 
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationFilter.java
+++ 
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationFilter.java
@@ -137,10 +137,7 @@ public class OidcRpAuthenticationFilter implements 
ContainerRequestFilter {
             return true;
         }
         URI base = rc.getUriInfo().getAbsolutePath();
-        return uri.getScheme() != null
-            && uri.getScheme().equalsIgnoreCase(base.getScheme())
-            && uri.getAuthority() != null
-            && uri.getAuthority().equalsIgnoreCase(base.getAuthority());
+        return OidcRpAuthenticationUtils.isSameOrigin(base, uri);
     }
     public void setRedirectUri(String redirectUri) {
         this.redirectUri = redirectUri;
diff --git 
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationService.java
 
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationService.java
index 30d957bf6a2..892278340d2 100644
--- 
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationService.java
+++ 
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationService.java
@@ -61,7 +61,7 @@ public class OidcRpAuthenticationService {
             String basePath = (String)mc.get("http.base.path");
             redirectUri = 
UriBuilder.fromUri(basePath).path(defaultLocation).build();
         } else if (location != null) {
-            redirectUri = URI.create(UrlUtils.urlDecode(location));
+            redirectUri = getSameOriginUri(location);
         }
         if (redirectUri != null) {
             return Response.seeOther(redirectUri).build();
@@ -69,6 +69,20 @@ public class OidcRpAuthenticationService {
         return Response.ok(oidcContext).build();
     }
 
+    private URI getSameOriginUri(String location) {
+        final URI uri;
+        try {
+            uri = URI.create(UrlUtils.urlDecode(location));
+        } catch (IllegalArgumentException ex) {
+            return null;
+        }
+        URI base = mc.getUriInfo().getAbsolutePath();
+        if (OidcRpAuthenticationUtils.isSameOrigin(base, uri)) {
+            return uri;
+        }
+        return null;
+    }
+
     public void setDefaultLocation(String defaultLocation) {
         this.defaultLocation = defaultLocation;
     }
diff --git 
a/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationUtils.java
 
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationUtils.java
new file mode 100644
index 00000000000..46f18dcc74a
--- /dev/null
+++ 
b/rt/rs/security/sso/oidc/src/main/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationUtils.java
@@ -0,0 +1,36 @@
+/**
+ * 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.oidc.rp;
+
+import java.net.URI;
+
+final class OidcRpAuthenticationUtils {
+    private OidcRpAuthenticationUtils() {
+    }
+
+    static boolean isSameOrigin(URI base, URI uri) {
+        if (uri.getScheme() == null && uri.getAuthority() == null) {
+            return true;
+        }
+        return uri.getScheme() != null
+            && uri.getScheme().equalsIgnoreCase(base.getScheme())
+            && uri.getAuthority() != null
+            && uri.getAuthority().equalsIgnoreCase(base.getAuthority());
+    }
+}
diff --git 
a/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationServiceTest.java
 
b/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationServiceTest.java
new file mode 100644
index 00000000000..2a17b10f53b
--- /dev/null
+++ 
b/rt/rs/security/sso/oidc/src/test/java/org/apache/cxf/rs/security/oidc/rp/OidcRpAuthenticationServiceTest.java
@@ -0,0 +1,120 @@
+/**
+ * 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.oidc.rp;
+
+import java.lang.reflect.Field;
+import java.net.URI;
+
+import jakarta.ws.rs.core.MultivaluedHashMap;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.UriInfo;
+import org.apache.cxf.common.util.UrlUtils;
+import org.apache.cxf.jaxrs.ext.MessageContext;
+import org.apache.cxf.jaxrs.ext.MessageContextImpl;
+import org.apache.cxf.message.ExchangeImpl;
+import org.apache.cxf.message.MessageImpl;
+import org.apache.cxf.rs.security.oauth2.client.ClientTokenContextManager;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class OidcRpAuthenticationServiceTest {
+    private static final URI REQUEST_URI = 
URI.create("https://app.example.com:8080/services/rp/complete";);
+
+    @Test
+    public void testRejectsCrossOriginRedirect() {
+        Response response = 
completeWithState("https://evil.example.com/phish";);
+
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+        assertNull(response.getHeaderString("Location"));
+    }
+
+    @Test
+    public void testRejectsProtocolRelativeRedirect() {
+        Response response = completeWithState("//evil.example.com/phish");
+
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+        assertNull(response.getHeaderString("Location"));
+    }
+
+    @Test
+    public void testRejectsDoubleEncodedCrossOriginRedirect() {
+        String attackerLocation = 
"https%253A%252F%252Fevil.example.com%252Fphish";
+        String callbackLocation = UrlUtils.urlDecode(attackerLocation);
+        Response response = completeWithState(callbackLocation);
+
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+        assertNull(response.getHeaderString("Location"));
+    }
+
+    @Test
+    public void testAllowsSameOriginAbsoluteRedirect() {
+        Response response = 
completeWithState("https://app.example.com:8080/services/protected";);
+
+        assertEquals(Response.Status.SEE_OTHER.getStatusCode(), 
response.getStatus());
+        assertEquals("https://app.example.com:8080/services/protected";,
+                     response.getHeaderString("Location"));
+    }
+
+    @Test
+    public void testAllowsRelativeRedirect() {
+        Response response = completeWithState("/services/protected");
+
+        assertEquals(Response.Status.SEE_OTHER.getStatusCode(), 
response.getStatus());
+        assertEquals("/services/protected", 
response.getHeaderString("Location"));
+    }
+
+    private Response completeWithState(String location) {
+        OidcClientTokenContext context = new OidcClientTokenContextImpl();
+        MultivaluedHashMap<String, String> state = new MultivaluedHashMap<>();
+        state.putSingle("state", location);
+        ((OidcClientTokenContextImpl)context).setState(state);
+
+        UriInfo uriInfo = mock(UriInfo.class);
+        when(uriInfo.getAbsolutePath()).thenReturn(REQUEST_URI);
+        MessageImpl message = new MessageImpl();
+        message.setExchange(new ExchangeImpl());
+        MessageContext messageContext = new MessageContextImpl(message) {
+            @Override
+            public UriInfo getUriInfo() {
+                return uriInfo;
+            }
+        };
+
+        OidcRpAuthenticationService service = new 
OidcRpAuthenticationService();
+        
service.setClientTokenContextManager(mock(ClientTokenContextManager.class));
+        setMessageContext(service, messageContext);
+        return service.completeAuthentication(context);
+    }
+
+    private void setMessageContext(OidcRpAuthenticationService service,
+                                   org.apache.cxf.jaxrs.ext.MessageContext 
messageContext) {
+        try {
+            Field field = 
OidcRpAuthenticationService.class.getDeclaredField("mc");
+            field.setAccessible(true);
+            field.set(service, messageContext);
+        } catch (ReflectiveOperationException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+}

Reply via email to