From d69e31e863138d7d09cbe93216d8da44eb9d43b5 Mon Sep 17 00:00:00 2001 From: tanya732 Date: Fri, 29 May 2026 13:38:06 +0530 Subject: [PATCH 1/3] Feat: Add withHttpClient support --- .../com/auth0/AuthenticationController.java | 30 ++ src/main/java/com/auth0/RequestProcessor.java | 34 ++- .../auth0/AuthenticationControllerTest.java | 44 +++ .../java/com/auth0/RequestProcessorTest.java | 261 ++++++++++++++++++ 4 files changed, 359 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/auth0/AuthenticationController.java b/src/main/java/com/auth0/AuthenticationController.java index c768765..7835b9f 100644 --- a/src/main/java/com/auth0/AuthenticationController.java +++ b/src/main/java/com/auth0/AuthenticationController.java @@ -1,6 +1,7 @@ package com.auth0; import com.auth0.jwk.JwkProvider; +import com.auth0.net.client.Auth0HttpClient; import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; @@ -73,6 +74,7 @@ public static class Builder { private final String clientSecret; private String responseType; private JwkProvider jwkProvider; + private Auth0HttpClient httpClient; private Integer clockSkew; private Integer authenticationMaxAge; private boolean useLegacySameSiteCookie; @@ -179,6 +181,31 @@ public Builder withJwkProvider(JwkProvider jwkProvider) { return this; } + /** + * Sets a custom {@link Auth0HttpClient} to use for all HTTP requests made by this library + * (token exchange, PAR, etc.). Use this to configure timeouts, proxies, or other HTTP settings. + * + *
{@code
+         * Auth0HttpClient httpClient = DefaultHttpClient.newBuilder()
+         *     .withConnectTimeout(10)
+         *     .withReadTimeout(10)
+         *     .build();
+         *
+         * AuthenticationController controller = AuthenticationController
+         *     .newBuilder(domain, clientId, clientSecret)
+         *     .withHttpClient(httpClient)
+         *     .build();
+         * }
+ * + * @param httpClient a configured {@link Auth0HttpClient} instance. + * @return this same builder instance. + */ + public Builder withHttpClient(Auth0HttpClient httpClient) { + Validate.notNull(httpClient); + this.httpClient = httpClient; + return this; + } + /** * Sets the clock-skew or leeway value to use in the ID Token verification. The value must be in seconds. * Defaults to 60 seconds. @@ -267,6 +294,9 @@ public AuthenticationController build() throws UnsupportedOperationException { if (jwkProvider != null) { builder.withJwkProvider(jwkProvider); } + if (httpClient != null) { + builder.withHttpClient(httpClient); + } return new AuthenticationController(builder.build()); } diff --git a/src/main/java/com/auth0/RequestProcessor.java b/src/main/java/com/auth0/RequestProcessor.java index 36a53cc..c1e9512 100644 --- a/src/main/java/com/auth0/RequestProcessor.java +++ b/src/main/java/com/auth0/RequestProcessor.java @@ -11,6 +11,7 @@ import com.auth0.jwk.JwkException; import com.auth0.jwk.JwkProvider; import com.auth0.jwk.UrlJwkProvider; +import com.auth0.net.client.Auth0HttpClient; import com.auth0.net.client.DefaultHttpClient; import com.auth0.utils.tokens.IdTokenVerifier; import com.auth0.utils.tokens.SignatureVerifier; @@ -50,6 +51,7 @@ class RequestProcessor { private final String clientId; private final String clientSecret; private final JwkProvider jwkProvider; + private Auth0HttpClient httpClient; private final Integer clockSkew; private final Integer authenticationMaxAge; @@ -71,6 +73,7 @@ static class Builder { private final String clientSecret; private JwkProvider jwkProvider; + private Auth0HttpClient httpClient; private boolean useLegacySameSiteCookie = true; private Integer clockSkew; private Integer authenticationMaxAge; @@ -93,6 +96,11 @@ Builder withJwkProvider(JwkProvider jwkProvider) { return this; } + Builder withHttpClient(Auth0HttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + public Builder withClockSkew(Integer clockSkew) { this.clockSkew = clockSkew; return this; @@ -125,13 +133,13 @@ Builder withInvitation(String invitation) { RequestProcessor build() { return new RequestProcessor(domainProvider, responseType, clientId, clientSecret, - jwkProvider, useLegacySameSiteCookie, clockSkew, authenticationMaxAge, + jwkProvider, httpClient, useLegacySameSiteCookie, clockSkew, authenticationMaxAge, organization, invitation, cookiePath); } } private RequestProcessor(DomainProvider domainProvider, String responseType, String clientId, - String clientSecret, JwkProvider jwkProvider, + String clientSecret, JwkProvider jwkProvider, Auth0HttpClient httpClient, boolean useLegacySameSiteCookie, Integer clockSkew, Integer authenticationMaxAge, String organization, String invitation, String cookiePath) { this.domainProvider = domainProvider; @@ -139,6 +147,7 @@ private RequestProcessor(DomainProvider domainProvider, String responseType, Str this.clientId = clientId; this.clientSecret = clientSecret; this.jwkProvider = jwkProvider; + this.httpClient = httpClient; this.useLegacySameSiteCookie = useLegacySameSiteCookie; this.clockSkew = clockSkew; this.authenticationMaxAge = authenticationMaxAge; @@ -156,18 +165,23 @@ void doNotSendTelemetry() { } AuthAPI createClientForDomain(String domain) { - DefaultHttpClient.Builder httpBuilder = DefaultHttpClient.newBuilder() - .telemetryEnabled(!telemetryDisabled); - - if (loggingEnabled) { - httpBuilder.withLogging(new LoggingOptions(LoggingOptions.LogLevel.BODY)); - } - return AuthAPI.newBuilder(domain, clientId, clientSecret) - .withHttpClient(httpBuilder.build()) + .withHttpClient(getHttpClient()) .build(); } + private Auth0HttpClient getHttpClient() { + if (this.httpClient == null) { + DefaultHttpClient.Builder httpBuilder = DefaultHttpClient.newBuilder() + .telemetryEnabled(!telemetryDisabled); + if (loggingEnabled) { + httpBuilder.withLogging(new LoggingOptions(LoggingOptions.LogLevel.BODY)); + } + this.httpClient = httpBuilder.build(); + } + return this.httpClient; + } + /** * Pre builds an Auth0 Authorize Url with the given redirect URI, state and nonce parameters. * diff --git a/src/test/java/com/auth0/AuthenticationControllerTest.java b/src/test/java/com/auth0/AuthenticationControllerTest.java index 4fc78ea..6af69ac 100644 --- a/src/test/java/com/auth0/AuthenticationControllerTest.java +++ b/src/test/java/com/auth0/AuthenticationControllerTest.java @@ -1,6 +1,7 @@ package com.auth0; import com.auth0.jwk.JwkProvider; +import com.auth0.net.client.Auth0HttpClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -31,6 +32,8 @@ public class AuthenticationControllerTest { @Mock private DomainResolver mockDomainResolver; @Mock + private Auth0HttpClient mockHttpClient; + @Mock private Tokens mockTokens; private HttpServletRequest request; @@ -82,6 +85,7 @@ public void shouldConfigureBuilderWithAllOptions() { AuthenticationController controller = AuthenticationController.newBuilder(DOMAIN, CLIENT_ID, CLIENT_SECRET) .withResponseType("id_token token") .withJwkProvider(mockJwkProvider) + .withHttpClient(mockHttpClient) .withClockSkew(120) .withAuthenticationMaxAge(3600) .withLegacySameSiteCookie(false) @@ -131,6 +135,7 @@ public void shouldValidateNullParameters() { assertThrows(NullPointerException.class, () -> builder.withDomain(null)); assertThrows(NullPointerException.class, () -> builder.withResponseType(null)); assertThrows(NullPointerException.class, () -> builder.withJwkProvider(null)); + assertThrows(NullPointerException.class, () -> builder.withHttpClient(null)); assertThrows(NullPointerException.class, () -> builder.withClockSkew(null)); assertThrows(NullPointerException.class, () -> builder.withAuthenticationMaxAge(null)); assertThrows(NullPointerException.class, () -> builder.withOrganization(null)); @@ -421,4 +426,43 @@ public void shouldHandleImplicitGrantResponseType() { assertThat(controller, is(notNullValue())); } + + // --- HttpClient Configuration Tests --- + + @Test + public void shouldBuildWithCustomHttpClient() { + AuthenticationController controller = AuthenticationController.newBuilder(DOMAIN, CLIENT_ID, CLIENT_SECRET) + .withHttpClient(mockHttpClient) + .build(); + + assertThat(controller, is(notNullValue())); + assertThat(controller.getRequestProcessor(), is(notNullValue())); + } + + @Test + public void shouldBuildWithCustomHttpClientAndJwkProvider() { + AuthenticationController controller = AuthenticationController.newBuilder(DOMAIN, CLIENT_ID, CLIENT_SECRET) + .withHttpClient(mockHttpClient) + .withJwkProvider(mockJwkProvider) + .build(); + + assertThat(controller, is(notNullValue())); + } + + @Test + public void shouldBuildWithCustomHttpClientAndDomainResolver() { + AuthenticationController controller = AuthenticationController + .newBuilder(mockDomainResolver, CLIENT_ID, CLIENT_SECRET) + .withHttpClient(mockHttpClient) + .build(); + + assertThat(controller, is(notNullValue())); + } + + @Test + public void shouldThrowExceptionWhenHttpClientIsNull() { + AuthenticationController.Builder builder = AuthenticationController.newBuilder(DOMAIN, CLIENT_ID, CLIENT_SECRET); + + assertThrows(NullPointerException.class, () -> builder.withHttpClient(null)); + } } diff --git a/src/test/java/com/auth0/RequestProcessorTest.java b/src/test/java/com/auth0/RequestProcessorTest.java index ec30c56..0851418 100644 --- a/src/test/java/com/auth0/RequestProcessorTest.java +++ b/src/test/java/com/auth0/RequestProcessorTest.java @@ -6,6 +6,7 @@ import com.auth0.jwk.JwkProvider; import com.auth0.net.Response; import com.auth0.net.TokenRequest; +import com.auth0.net.client.Auth0HttpClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -44,6 +45,8 @@ public class RequestProcessorTest { @Mock private JwkProvider mockJwkProvider; @Mock + private Auth0HttpClient mockHttpClient; + @Mock private AuthAPI mockAuthAPI; @Mock private TokenRequest mockTokenRequest; @@ -646,6 +649,264 @@ public void shouldSupportAuthenticationMaxAge() { assertThat(processor, is(notNullValue())); } + // --- Custom HttpClient Tests --- + + @Test + public void shouldBuildWithCustomHttpClient() { + RequestProcessor processor = new RequestProcessor.Builder( + mockDomainProvider, + RESPONSE_TYPE_CODE, + CLIENT_ID, + CLIENT_SECRET) + .withHttpClient(mockHttpClient) + .build(); + + assertThat(processor, is(notNullValue())); + } + + @Test + public void shouldCreateClientForDomainWithCustomHttpClient() { + RequestProcessor processor = new RequestProcessor.Builder( + mockDomainProvider, + RESPONSE_TYPE_CODE, + CLIENT_ID, + CLIENT_SECRET) + .withHttpClient(mockHttpClient) + .build(); + + AuthAPI client = processor.createClientForDomain(DOMAIN); + assertThat(client, is(notNullValue())); + } + + @Test + public void shouldReuseCustomHttpClientAcrossMultipleDomains() { + RequestProcessor processor = new RequestProcessor.Builder( + mockDomainProvider, + RESPONSE_TYPE_CODE, + CLIENT_ID, + CLIENT_SECRET) + .withHttpClient(mockHttpClient) + .build(); + + AuthAPI client1 = processor.createClientForDomain("domain1.auth0.com"); + AuthAPI client2 = processor.createClientForDomain("domain2.auth0.com"); + + assertThat(client1, is(notNullValue())); + assertThat(client2, is(notNullValue())); + } + + @Test + public void shouldCreateDefaultHttpClientWhenNoneProvided() { + RequestProcessor processor = new RequestProcessor.Builder( + mockDomainProvider, + RESPONSE_TYPE_CODE, + CLIENT_ID, + CLIENT_SECRET) + .build(); + + AuthAPI client = processor.createClientForDomain(DOMAIN); + assertThat(client, is(notNullValue())); + } + + @Test + public void shouldReuseDefaultHttpClientAcrossMultipleCalls() { + RequestProcessor processor = new RequestProcessor.Builder( + mockDomainProvider, + RESPONSE_TYPE_CODE, + CLIENT_ID, + CLIENT_SECRET) + .build(); + + AuthAPI client1 = processor.createClientForDomain("domain1.auth0.com"); + AuthAPI client2 = processor.createClientForDomain("domain2.auth0.com"); + + assertThat(client1, is(notNullValue())); + assertThat(client2, is(notNullValue())); + } + + @Test + public void shouldBuildWithHttpClientAndJwkProvider() { + RequestProcessor processor = new RequestProcessor.Builder( + mockDomainProvider, + RESPONSE_TYPE_CODE, + CLIENT_ID, + CLIENT_SECRET) + .withHttpClient(mockHttpClient) + .withJwkProvider(mockJwkProvider) + .build(); + + assertThat(processor, is(notNullValue())); + } + + // --- Transaction-Keyed Cookie Tests --- + + @Test + public void shouldValidateStateFromTransactionKeyedCookie() { + when(mockDomainProvider.getDomain(any())).thenReturn(DOMAIN); + + Map params = new HashMap<>(); + params.put("state", "txn-state-123"); + params.put("code", "auth-code"); + MockHttpServletRequest request = getRequest(params); + // Transaction-keyed cookie: com.auth0.state.{state_value} + request.setCookies(new Cookie("com.auth0.state.txn-state-123", "txn-state-123")); + + when(mockTokenHolder.getIdToken()).thenReturn(null); + when(mockTokenHolder.getAccessToken()).thenReturn("access"); + when(mockTokenResponse.getBody()).thenReturn(mockTokenHolder); + try { + when(mockTokenRequest.execute()).thenReturn(mockTokenResponse); + } catch (Auth0Exception e) { + fail("Unexpected exception"); + } + when(mockAuthAPI.exchangeCode(eq("auth-code"), anyString())).thenReturn(mockTokenRequest); + + RequestProcessor handler = createDefaultRequestProcessor(); + RequestProcessor spy = spy(handler); + doReturn(mockAuthAPI).when(spy).createClientForDomain(anyString()); + + assertDoesNotThrow(() -> spy.process(request, response)); + } + + @Test + public void shouldFallbackToLegacyStateCookieWhenTransactionKeyedMissing() { + when(mockDomainProvider.getDomain(any())).thenReturn(DOMAIN); + + Map params = new HashMap<>(); + params.put("state", "legacy-state-456"); + params.put("code", "auth-code"); + MockHttpServletRequest request = getRequest(params); + // Legacy fixed-name cookie (v1 compatibility) + request.setCookies(new Cookie("com.auth0.state", "legacy-state-456")); + + when(mockTokenHolder.getIdToken()).thenReturn(null); + when(mockTokenHolder.getAccessToken()).thenReturn("access"); + when(mockTokenResponse.getBody()).thenReturn(mockTokenHolder); + try { + when(mockTokenRequest.execute()).thenReturn(mockTokenResponse); + } catch (Auth0Exception e) { + fail("Unexpected exception"); + } + when(mockAuthAPI.exchangeCode(eq("auth-code"), anyString())).thenReturn(mockTokenRequest); + + RequestProcessor handler = createDefaultRequestProcessor(); + RequestProcessor spy = spy(handler); + doReturn(mockAuthAPI).when(spy).createClientForDomain(anyString()); + + assertDoesNotThrow(() -> spy.process(request, response)); + } + + @Test + public void shouldRejectWhenNoStateCookieExists() { + Map params = new HashMap<>(); + params.put("state", "orphan-state"); + MockHttpServletRequest request = getRequest(params); + // No cookies at all + + RequestProcessor handler = createDefaultRequestProcessor(); + + InvalidRequestException e = assertThrows(InvalidRequestException.class, () -> handler.process(request, response)); + assertThat(e.getCode(), is("a0.invalid_state")); + } + + // --- MCD Origin Domain Binding Tests --- + + @Test + public void shouldUseDomainFromSignedCookieWhenPresent() throws Exception { + String state = "mcd-state-789"; + String domain = "brand-a.auth0.com"; + + // Create a signed origin domain cookie + String signedDomain = SignedCookieUtils.sign(domain, state, CLIENT_SECRET); + + Map params = new HashMap<>(); + params.put("state", state); + params.put("code", "auth-code"); + MockHttpServletRequest request = getRequest(params); + request.setCookies( + new Cookie("com.auth0.state." + state, state), + new Cookie("com.auth0.origin_domain", signedDomain) + ); + + when(mockDomainProvider.getDomain(any())).thenReturn("fallback.auth0.com"); + when(mockTokenHolder.getIdToken()).thenReturn(null); + when(mockTokenHolder.getAccessToken()).thenReturn("access"); + when(mockTokenResponse.getBody()).thenReturn(mockTokenHolder); + when(mockTokenRequest.execute()).thenReturn(mockTokenResponse); + when(mockAuthAPI.exchangeCode(eq("auth-code"), anyString())).thenReturn(mockTokenRequest); + + RequestProcessor handler = createDefaultRequestProcessor(); + RequestProcessor spy = spy(handler); + doReturn(mockAuthAPI).when(spy).createClientForDomain(anyString()); + + spy.process(request, response); + + // Should use the domain from the signed cookie, not the fallback + verify(spy).createClientForDomain(domain); + } + + @Test + public void shouldFallbackToDomainProviderWhenSignedCookieMissing() throws Exception { + String state = "no-cookie-state"; + String fallbackDomain = "fallback.auth0.com"; + + Map params = new HashMap<>(); + params.put("state", state); + params.put("code", "auth-code"); + MockHttpServletRequest request = getRequest(params); + request.setCookies(new Cookie("com.auth0.state." + state, state)); + + when(mockDomainProvider.getDomain(any())).thenReturn(fallbackDomain); + when(mockTokenHolder.getIdToken()).thenReturn(null); + when(mockTokenHolder.getAccessToken()).thenReturn("access"); + when(mockTokenResponse.getBody()).thenReturn(mockTokenHolder); + when(mockTokenRequest.execute()).thenReturn(mockTokenResponse); + when(mockAuthAPI.exchangeCode(eq("auth-code"), anyString())).thenReturn(mockTokenRequest); + + RequestProcessor handler = createDefaultRequestProcessor(); + RequestProcessor spy = spy(handler); + doReturn(mockAuthAPI).when(spy).createClientForDomain(anyString()); + + spy.process(request, response); + + // Should fall back to domainProvider when no signed origin cookie + verify(spy).createClientForDomain(fallbackDomain); + } + + @Test + public void shouldFallbackToDomainProviderWhenSignedCookieTampered() throws Exception { + String state = "tampered-state"; + String fallbackDomain = "fallback.auth0.com"; + + // Tampered cookie — signed with different state + String signedDomain = SignedCookieUtils.sign("evil.auth0.com", "different-state", CLIENT_SECRET); + + Map params = new HashMap<>(); + params.put("state", state); + params.put("code", "auth-code"); + MockHttpServletRequest request = getRequest(params); + request.setCookies( + new Cookie("com.auth0.state." + state, state), + new Cookie("com.auth0.origin_domain", signedDomain) + ); + + when(mockDomainProvider.getDomain(any())).thenReturn(fallbackDomain); + when(mockTokenHolder.getIdToken()).thenReturn(null); + when(mockTokenHolder.getAccessToken()).thenReturn("access"); + when(mockTokenResponse.getBody()).thenReturn(mockTokenHolder); + when(mockTokenRequest.execute()).thenReturn(mockTokenResponse); + when(mockAuthAPI.exchangeCode(eq("auth-code"), anyString())).thenReturn(mockTokenRequest); + + RequestProcessor handler = createDefaultRequestProcessor(); + RequestProcessor spy = spy(handler); + doReturn(mockAuthAPI).when(spy).createClientForDomain(anyString()); + + spy.process(request, response); + + // Tampered cookie should be rejected, fallback to domainProvider + verify(spy).createClientForDomain(fallbackDomain); + } + // --- Helper Methods --- private RequestProcessor createDefaultRequestProcessor() { From 3554fd2c4570854a2ba336ac6c459880f9477559 Mon Sep 17 00:00:00 2001 From: Tanya Sinha Date: Fri, 29 May 2026 14:36:05 +0530 Subject: [PATCH 2/3] Potential fix for pull request finding 'CodeQL / Deprecated method or constructor invocation' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/main/java/com/auth0/AuthenticationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/auth0/AuthenticationController.java b/src/main/java/com/auth0/AuthenticationController.java index 7835b9f..095d688 100644 --- a/src/main/java/com/auth0/AuthenticationController.java +++ b/src/main/java/com/auth0/AuthenticationController.java @@ -201,7 +201,7 @@ public Builder withJwkProvider(JwkProvider jwkProvider) { * @return this same builder instance. */ public Builder withHttpClient(Auth0HttpClient httpClient) { - Validate.notNull(httpClient); + Validate.notNull(httpClient, "httpClient must not be null"); this.httpClient = httpClient; return this; } From 4b591284af6c00bc4edd2a150a71f358ec68f6cf Mon Sep 17 00:00:00 2001 From: tanya732 Date: Fri, 29 May 2026 14:40:09 +0530 Subject: [PATCH 3/3] Updated javadoc --- src/main/java/com/auth0/AuthenticationController.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/auth0/AuthenticationController.java b/src/main/java/com/auth0/AuthenticationController.java index 7835b9f..ba066e2 100644 --- a/src/main/java/com/auth0/AuthenticationController.java +++ b/src/main/java/com/auth0/AuthenticationController.java @@ -185,10 +185,18 @@ public Builder withJwkProvider(JwkProvider jwkProvider) { * Sets a custom {@link Auth0HttpClient} to use for all HTTP requests made by this library * (token exchange, PAR, etc.). Use this to configure timeouts, proxies, or other HTTP settings. * + *

Note: When a custom {@code Auth0HttpClient} is provided, the + * {@link AuthenticationController#setLoggingEnabled(boolean)} and + * {@link AuthenticationController#doNotSendTelemetry()} settings will have no effect, + * as those are configured at the HTTP client level. You should configure logging and + * telemetry directly on the client instance before passing it here.

+ * *
{@code
          * Auth0HttpClient httpClient = DefaultHttpClient.newBuilder()
          *     .withConnectTimeout(10)
          *     .withReadTimeout(10)
+         *     .telemetryEnabled(false)
+         *     .withLogging(new LoggingOptions(LoggingOptions.LogLevel.BODY))
          *     .build();
          *
          * AuthenticationController controller = AuthenticationController