From 71060f2646a485e51c548889f52fe60f3efabc89 Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Sun, 19 Jul 2026 11:53:46 -0400 Subject: [PATCH 1/2] Validate redirect URIs according to OpenID Connect Dynamic Client Registration 1.0 specification --- .../services/DynamicRegistrationService.java | 47 ++++++++++- .../DynamicRegistrationServiceTest.java | 77 +++++++++++++++++++ 2 files changed, 121 insertions(+), 3 deletions(-) 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..81f2b0e8fbd 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,6 +18,7 @@ */ package org.apache.cxf.rs.security.oauth2.services; +import java.net.URI; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -42,6 +43,7 @@ import org.apache.cxf.jaxrs.ext.MessageContext; import org.apache.cxf.jaxrs.utils.ExceptionUtils; import org.apache.cxf.jaxrs.utils.JAXRSUtils; +import org.apache.cxf.resource.URIResolver; 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.common.UserSubject; @@ -54,8 +56,11 @@ @Path("register") public class DynamicRegistrationService { + private static final List LOOPBACK_HOSTS = List.of("localhost", "127.0.0.1", "[::1]"); + private static final String WEB_APPLICATION_TYPE = "web"; + private static final String NATIVE_APPLICATION_TYPE = "native"; private static final String INVALID_CLIENT_METADATA = "invalid_client_metadata"; - private static final String DEFAULT_APPLICATION_TYPE = "web"; + private static final String DEFAULT_APPLICATION_TYPE = WEB_APPLICATION_TYPE; private static final Integer DEFAULT_CLIENT_ID_SIZE = 10; private ClientRegistrationProvider clientProvider; private String initialAccessToken; @@ -181,7 +186,7 @@ protected ClientRegistration fromClientToClientRegistration(Client c) { ClientRegistration reg = new ClientRegistration(); reg.setClientName(c.getApplicationName()); reg.setGrantTypes(c.getAllowedGrantTypes()); - reg.setApplicationType(c.isConfidential() ? "web" : "native"); + reg.setApplicationType(c.isConfidential() ? WEB_APPLICATION_TYPE : NATIVE_APPLICATION_TYPE); if (!c.getRedirectUris().isEmpty()) { reg.setRedirectUris(c.getRedirectUris()); } @@ -366,12 +371,48 @@ protected boolean isPasswordRequired(List grantTypes, String tokenEndpoi || OAuthConstants.TOKEN_ENDPOINT_AUTH_POST.equals(tokenEndpointAuthMethod)); } + @SuppressWarnings("PMD.CollapsibleIfStatements") protected void validateRequestUri(String uri, String appType, List grantTypes) { + if (uri == null || uri.isBlank()) { + reportInvalidRequestError(new OAuthError(INVALID_CLIENT_METADATA, "Empty redirect URI is not supported")); + } + // Web Clients using the OAuth Implicit Grant Type MUST only register URLs using the https scheme // as redirect_uris; they MUST NOT use localhost as the hostname. Native Clients MUST only register - // redirect_uris using custom URI schemes or URLs using the http: scheme with localhost as the hostname. + // redirect_uris using custom URI schemes or loopback URLs using the http scheme; loopback URLs use + // localhost or the IP loopback literals 127.0.0.1 or [::1] as the hostname. // Authorization Servers MAY place additional constraints on Native Clients. Authorization Servers MAY // reject Redirection URI values using the http scheme, other than the localhost case for Native Clients + + final URI parsedUri = URI.create(uri); + if (parsedUri.getHost() == null || parsedUri.getScheme() == null) { + reportInvalidRequestError(new OAuthError(INVALID_CLIENT_METADATA, "Unsupported redirect URI scheme/host")); + } + final String host = parsedUri.getHost().toLowerCase(); + final String scheme = parsedUri.getScheme().toLowerCase(); + if (!URIResolver.getAllowedSchemes().contains(scheme)) { + reportInvalidRequestError(new OAuthError(INVALID_CLIENT_METADATA, + "Redirect URI scheme is not allowed: " + scheme + + ". Allowed schemes: " + URIResolver.getAllowedSchemes())); + } + + // Kind of the application. The default, if omitted, is web. The defined values are native or web. + if (appType == null || appType.isBlank() || appType.equalsIgnoreCase(WEB_APPLICATION_TYPE)) { + if (grantTypes.contains(OAuthConstants.IMPLICIT_GRANT)) { + if (!"https".equalsIgnoreCase(scheme)) { + reportInvalidRequestError(new OAuthError(INVALID_CLIENT_METADATA, + "Unsupported redirect URI scheme")); + } else if ("localhost".equals(host)) { + reportInvalidRequestError(new OAuthError(INVALID_CLIENT_METADATA, + "Unsupported redirect URI hostname")); + } + } + } else if (appType.equalsIgnoreCase(NATIVE_APPLICATION_TYPE)) { + if ("http".equalsIgnoreCase(scheme) && !LOOPBACK_HOSTS.contains(host)) { + reportInvalidRequestError(new OAuthError(INVALID_CLIENT_METADATA, + "Unsupported redirect URI hostname for scheme")); + } + } } public void setClientProvider(ClientRegistrationProvider clientProvider) { 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..e8a2040cd6f 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 @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collections; +import java.util.List; import jakarta.ws.rs.BadRequestException; import org.apache.cxf.rs.security.oauth2.common.Client; @@ -80,6 +81,82 @@ public void testAcceptsRegisteredScopesWhenAllowlistNotConfigured() { assertEquals(Collections.singletonList("openid"), client.getRegisteredScopes()); } + @Test + public void testAcceptsAllowedRedirectUrlsWebApp() { + TestDynamicRegistrationService service = new TestDynamicRegistrationService(); + + ClientRegistration request = new ClientRegistration(); + request.setScope("read write"); + request.setRedirectUris(List.of("https://localhost", "http://localhost")); + + Client client = createClient(); + service.applyClientRegistration(request, client); + + assertEquals(Arrays.asList("https://localhost", "http://localhost"), client.getRedirectUris()); + } + + @Test + public void testRejectsNotAllowedRedirectUrlsWebApp() { + TestDynamicRegistrationService service = new TestDynamicRegistrationService(); + + final List schemes = List.of("http", "https"); + for (String scheme: schemes) { + ClientRegistration request = new ClientRegistration(); + request.setScope("read write"); + request.setRedirectUris(List.of(scheme + "://localhost")); + + Client client = createClient(); + client.setAllowedGrantTypes(Collections.singletonList(OAuthConstants.IMPLICIT_GRANT)); + assertThrows(BadRequestException.class, () -> service.applyClientRegistration(request, client)); + } + } + + @Test + public void testAcceptsAllowedRedirectUrlsNativeApp() { + TestDynamicRegistrationService service = new TestDynamicRegistrationService(); + + final List hosts = List.of("localhost", "127.0.0.1", "[::1]"); + for (String host: hosts) { + ClientRegistration request = new ClientRegistration(); + request.setScope("read write"); + request.setRedirectUris(List.of("http://" + host)); + request.setApplicationType("native"); + + Client client = createClient(); + service.applyClientRegistration(request, client); + + assertEquals(Arrays.asList("http://" + host), client.getRedirectUris()); + } + } + + @Test + public void testRejectsNotAllowedRedirectUrlsNativeApp() { + TestDynamicRegistrationService service = new TestDynamicRegistrationService(); + + ClientRegistration request = new ClientRegistration(); + request.setScope("read write"); + request.setRedirectUris(List.of("http://test")); + request.setApplicationType("native"); + + Client client = createClient(); + assertThrows(BadRequestException.class, () -> service.applyClientRegistration(request, client)); + } + + @Test + public void testRejectsNotAllowedRedirectUrls() { + TestDynamicRegistrationService service = new TestDynamicRegistrationService(); + + final List uris = List.of("custom://test", "//test", ""); + for (String uri: uris) { + ClientRegistration request = new ClientRegistration(); + request.setScope("read write"); + request.setRedirectUris(List.of(uri)); + + Client client = createClient(); + assertThrows(BadRequestException.class, () -> service.applyClientRegistration(request, client)); + } + } + private static Client createClient() { Client client = new Client("client", "secret", true); client.setAllowedGrantTypes(Collections.singletonList(OAuthConstants.CLIENT_CREDENTIALS_GRANT)); From 1f0a47a3382a2a39cdbe9cd0349d8c97335d7a6b Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Mon, 20 Jul 2026 07:02:51 -0400 Subject: [PATCH 2/2] Address code review comments --- .../oauth2/services/DynamicRegistrationService.java | 9 ++++----- .../oauth2/services/DynamicRegistrationServiceTest.java | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) 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 81f2b0e8fbd..838e517de28 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 @@ -43,7 +43,6 @@ import org.apache.cxf.jaxrs.ext.MessageContext; import org.apache.cxf.jaxrs.utils.ExceptionUtils; import org.apache.cxf.jaxrs.utils.JAXRSUtils; -import org.apache.cxf.resource.URIResolver; 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.common.UserSubject; @@ -390,16 +389,16 @@ protected void validateRequestUri(String uri, String appType, List grant } final String host = parsedUri.getHost().toLowerCase(); final String scheme = parsedUri.getScheme().toLowerCase(); - if (!URIResolver.getAllowedSchemes().contains(scheme)) { + if (!scheme.equals("http") && !scheme.equals("https")) { reportInvalidRequestError(new OAuthError(INVALID_CLIENT_METADATA, "Redirect URI scheme is not allowed: " + scheme - + ". Allowed schemes: " + URIResolver.getAllowedSchemes())); + + ". Allowed schemes: http, https")); } // Kind of the application. The default, if omitted, is web. The defined values are native or web. if (appType == null || appType.isBlank() || appType.equalsIgnoreCase(WEB_APPLICATION_TYPE)) { if (grantTypes.contains(OAuthConstants.IMPLICIT_GRANT)) { - if (!"https".equalsIgnoreCase(scheme)) { + if (!"https".equals(scheme)) { reportInvalidRequestError(new OAuthError(INVALID_CLIENT_METADATA, "Unsupported redirect URI scheme")); } else if ("localhost".equals(host)) { @@ -408,7 +407,7 @@ protected void validateRequestUri(String uri, String appType, List grant } } } else if (appType.equalsIgnoreCase(NATIVE_APPLICATION_TYPE)) { - if ("http".equalsIgnoreCase(scheme) && !LOOPBACK_HOSTS.contains(host)) { + if ("http".equals(scheme) && !LOOPBACK_HOSTS.contains(host)) { reportInvalidRequestError(new OAuthError(INVALID_CLIENT_METADATA, "Unsupported redirect URI hostname for scheme")); } 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 e8a2040cd6f..24d23caced9 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 @@ -146,7 +146,7 @@ public void testRejectsNotAllowedRedirectUrlsNativeApp() { public void testRejectsNotAllowedRedirectUrls() { TestDynamicRegistrationService service = new TestDynamicRegistrationService(); - final List uris = List.of("custom://test", "//test", ""); + final List uris = List.of("custom://test", "//test", "http:/"); for (String uri: uris) { ClientRegistration request = new ClientRegistration(); request.setScope("read write");