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); + } + } +}
