From 286fbc43e53483ff35175935378ad383258b1c2b Mon Sep 17 00:00:00 2001 From: h3nr1-d14z <166919885+h3nr1-d14z@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:15:20 +0700 Subject: [PATCH] Fix proxy credential registration when authentication and proxyAuthentication are combined CredentialBasicAuthentication.addCredentials() stored the target credential instead of the passed proxy credential, so requests through an authenticating proxy offered the wrong Basic credentials and the proxy kept answering 407. Store the credential that was passed in. Adds a socket-level fake authenticating proxy and regression tests covering proxyAuthentication alone and combined with basic authentication (the case that was broken). Fixes #314 --- .../auth/CredentialBasicAuthentication.java | 2 +- .../http_request/ProxyAuthenticationTest.java | 193 ++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 src/test/java/jenkins/plugins/http_request/ProxyAuthenticationTest.java diff --git a/src/main/java/jenkins/plugins/http_request/auth/CredentialBasicAuthentication.java b/src/main/java/jenkins/plugins/http_request/auth/CredentialBasicAuthentication.java index 1085221a..57ecaffb 100644 --- a/src/main/java/jenkins/plugins/http_request/auth/CredentialBasicAuthentication.java +++ b/src/main/java/jenkins/plugins/http_request/auth/CredentialBasicAuthentication.java @@ -40,7 +40,7 @@ public void addCredentials(HttpHost host, StandardUsernamePasswordCredentials cr if (host == null || credentials == null) { throw new IllegalArgumentException("Null target host or credentials"); } - extraCredentials.put(host, credential); + extraCredentials.put(host, credentials); } @Override diff --git a/src/test/java/jenkins/plugins/http_request/ProxyAuthenticationTest.java b/src/test/java/jenkins/plugins/http_request/ProxyAuthenticationTest.java new file mode 100644 index 00000000..9e2625b6 --- /dev/null +++ b/src/test/java/jenkins/plugins/http_request/ProxyAuthenticationTest.java @@ -0,0 +1,193 @@ +package jenkins.plugins.http_request; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +import hudson.model.Result; + +/** + * Verifies that requests sent through an authenticating proxy carry the proxy + * credential in the {@code Proxy-Authorization} header (issue #314). + * + *
Uses a minimal socket-level proxy rather than a real one: it rejects + * requests whose {@code Proxy-Authorization} does not match the expected + * credential with {@code 407 Proxy Authentication Required}. To keep a + * wrong-credential request from looping forever it downgrades to {@code 403} + * after a few challenged attempts.
+ */ +@WithJenkins +class ProxyAuthenticationTest extends HttpRequestTestBase { + + private static final String PROXY_USER = "proxyUser1"; + private static final String PROXY_PASSWORD = "proxyPassword1"; + private static final String TARGET_USER = "targetUser1"; + private static final String TARGET_PASSWORD = "targetPassword1"; + + @Test + @Timeout(180) + void proxyAuthenticationWithoutTargetAuthSendsProxyCredentials() throws Exception { + try (AuthenticatingProxy proxy = new AuthenticatingProxy()) { + registerBasicCredential("proxy-creds", PROXY_USER, PROXY_PASSWORD); + + WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proxyOnly"); + proj.setDefinition(new CpsFlowDefinition( + "def response = httpRequest url:'" + baseURL() + "/StatusOk',\n" + + " httpProxy: 'http://127.0.0.1:" + proxy.port() + "',\n" + + " proxyAuthentication: 'proxy-creds'\n" + + "println('Status: '+response.getStatus())\n", + true)); + + WorkflowRun run = proj.scheduleBuild2(0).get(); + + j.assertBuildStatusSuccess(run); + j.assertLogContains("Status: 200", run); + assertThat(proxy.lastProxyAuthorization(), is(proxy.expectedAuthorization())); + } + } + + @Test + @Timeout(180) + void proxyAuthenticationWithBasicTargetAuthSendsProxyCredentials() throws Exception { + try (AuthenticatingProxy proxy = new AuthenticatingProxy()) { + registerBasicCredential("proxy-creds", PROXY_USER, PROXY_PASSWORD); + registerBasicCredential("target-creds", TARGET_USER, TARGET_PASSWORD); + + WorkflowJob proj = j.jenkins.createProject(WorkflowJob.class, "proxyAndTarget"); + proj.setDefinition(new CpsFlowDefinition( + "def response = httpRequest url:'" + baseURL() + "/StatusOk',\n" + + " httpProxy: 'http://127.0.0.1:" + proxy.port() + "',\n" + + " proxyAuthentication: 'proxy-creds',\n" + + " authentication: 'target-creds'\n" + + "println('Status: '+response.getStatus())\n", + true)); + + WorkflowRun run = proj.scheduleBuild2(0).get(); + + j.assertBuildStatus(Result.SUCCESS, run); + j.assertLogContains("Status: 200", run); + // The proxy must receive the proxy credential, not the target one + assertThat(proxy.lastProxyAuthorization(), is(proxy.expectedAuthorization())); + } + } + + /** + * Minimal authenticating HTTP proxy. Serves requests on a loopback socket, + * answering {@code 200} when {@code Proxy-Authorization} matches the + * expected Basic credential, {@code 407} otherwise. + */ + private static final class AuthenticatingProxy implements Closeable { + private final ServerSocket serverSocket; + private final String expectedAuthorization; + private final AtomicReference