Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jenkins Plugin not able to retrieve infomation about user behind proxies requiring authentication #143

Merged
merged 7 commits into from
Dec 25, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@ of this software and associated documentation files (the "Software"), to deal

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.OkUrlFactory;
import hudson.ProxyConfiguration;
import okhttp3.Challenge;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import hudson.model.Item;
import hudson.security.Permission;
import hudson.security.SecurityRealm;
import jenkins.model.Jenkins;
import okhttp3.Request;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.providers.AbstractAuthenticationToken;
Expand All @@ -49,14 +52,14 @@ of this software and associated documentation files (the "Software"), to deal
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.github.RateLimitHandler;
import org.kohsuke.github.extras.OkHttpConnector;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
Expand All @@ -68,6 +71,7 @@ of this software and associated documentation files (the "Software"), to deal

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector;


/**
Expand Down Expand Up @@ -289,13 +293,16 @@ GitHub getGitHub() throws IOException {
throw new IOException("Invalid GitHub API URL: " + this.githubServer, e);
}

OkHttpClient client = new OkHttpClient().setProxy(getProxy(host));
OkHttpClient client = new OkHttpClient.Builder()
.proxy(getProxy(host))
.proxyAuthenticator(new JenkinsProxyAuthenticator(Jenkins.get().getProxy()))
.build();

this.gh = GitHubBuilder.fromEnvironment()
.withEndpoint(this.githubServer)
.withOAuthToken(this.accessToken)
.withRateLimitHandler(RateLimitHandler.FAIL)
.withConnector(new OkHttpConnector(new OkUrlFactory(client)))
.withConnector(new OkHttpGitHubConnector(client))
.build();
}
return gh;
Expand Down Expand Up @@ -480,7 +487,7 @@ private GHMyself loadMyself(@NonNull String token) throws IOException {
usersByIdCache.put(username, new GithubUser(ghMyself));
}
} catch (IOException e) {
LOGGER.log(Level.FINEST, e.getMessage(), e);
LOGGER.log(Level.INFO, e.getMessage(), e);
me = UNKNOWN_TOKEN;
usersByTokenCache.put(token, UNKNOWN_TOKEN);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.jenkinsci.plugins;

import hudson.ProxyConfiguration;
import hudson.util.Secret;
import okhttp3.Authenticator;
import okhttp3.Challenge;
import okhttp3.Credentials;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.logging.Level;
import java.util.logging.Logger;

public class JenkinsProxyAuthenticator implements Authenticator {


private static final Logger LOGGER = Logger
.getLogger(JenkinsProxyAuthenticator.class.getName());

private final ProxyConfiguration proxy;

public JenkinsProxyAuthenticator(ProxyConfiguration proxy) {
this.proxy = proxy;
}

@Nullable
@Override
public Request authenticate(@Nullable Route route, @NotNull Response response) {

if(response.request().header("Proxy-Authorization") != null) {
return null; // Give up since we already tried to authenticate
}

if(response.challenges().isEmpty()) {
// Proxy does not require authentication
return null;
}

// Refuse pre-emptive challenge
if(response.challenges().size() == 1) {
Challenge challenge = response.challenges().get(0);
if(challenge.scheme().equalsIgnoreCase("OkHttp-Preemptive")) {
return null;
}
}

for(Challenge challenge : response.challenges()) {
if(challenge.scheme().equalsIgnoreCase("Basic")) {
String username = proxy.getUserName();
Secret password = proxy.getSecretPassword();
if(username != null && password != null) {
String credentials = Credentials.basic(username, password.getPlainText());
return response.request()
.newBuilder()
.header("Proxy-Authorization", credentials)
.build();
} else {
LOGGER.log(Level.WARNING, "Proxy requires Basic authentication but no username and password have been configured for the proxy");
}
break;
}
}

LOGGER.log(Level.WARNING, "Proxy requires authentication, but does not support Basic authentication");
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.github.RateLimitHandler;
import org.kohsuke.github.extras.OkHttpConnector;
import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
Expand Down Expand Up @@ -71,7 +72,7 @@ private GHMyself mockGHMyselfAs(MockedStatic<GitHubBuilder> mockedGitHubBuilder,
Mockito.when(builder.withEndpoint("https://api.github.com")).thenReturn(builder);
Mockito.when(builder.withOAuthToken("accessToken")).thenReturn(builder);
Mockito.when(builder.withRateLimitHandler(RateLimitHandler.FAIL)).thenReturn(builder);
Mockito.when(builder.withConnector(Mockito.any(OkHttpConnector.class))).thenReturn(builder);
Mockito.when(builder.withConnector(Mockito.any(OkHttpGitHubConnector.class))).thenReturn(builder);
Mockito.when(builder.build()).thenReturn(gh);
GHMyself me = Mockito.mock(GHMyself.class);
Mockito.when(gh.getMyself()).thenReturn(me);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ of this software and associated documentation files (the "Software"), to deal
import org.kohsuke.github.PagedIterable;
import org.kohsuke.github.RateLimitHandler;
import org.kohsuke.github.extras.OkHttpConnector;
import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.mockito.Mock;
Expand Down Expand Up @@ -161,7 +162,7 @@ private GHMyself mockGHMyselfAs(MockedStatic<GitHubBuilder> mockedGitHubBuilder,
Mockito.when(builder.withEndpoint("https://api.github.com")).thenReturn(builder);
Mockito.when(builder.withOAuthToken("accessToken")).thenReturn(builder);
Mockito.when(builder.withRateLimitHandler(RateLimitHandler.FAIL)).thenReturn(builder);
Mockito.when(builder.withConnector(Mockito.any(OkHttpConnector.class))).thenReturn(builder);
Mockito.when(builder.withConnector(Mockito.any(OkHttpGitHubConnector.class))).thenReturn(builder);
Mockito.when(builder.build()).thenReturn(gh);
GHMyself me = Mockito.mock(GHMyself.class);
Mockito.when(gh.getMyself()).thenReturn(me);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.jenkinsci.plugins;

import hudson.ProxyConfiguration;
import okhttp3.Credentials;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import org.junit.Assert;
import org.junit.Test;


public class JenkinsProxyAuthenticatorTest {


@Test
public void refusesChallengeIfAuthenticationAlreadyFailed() {
Request previousRequest = new Request.Builder()
.url("https://example.com")
.header("Proxy-Authorization", "notNull")
.build();

Response response = new Response.Builder()
.code(407)
.request(previousRequest)
.protocol(Protocol.HTTP_1_0)
.message("Unauthorized")
.build();

Assert.assertNull(new JenkinsProxyAuthenticator(null).authenticate(null, response));
}

@Test
public void refusesPreemptiveOkHttpChallenge() {
Request previousRequest = new Request.Builder()
.url("https://example.com")
.build();

Response response = new Response.Builder()
.request(previousRequest)
.header("Proxy-Authenticate", "OkHttp-Preemptive")
.code(407)
.protocol(Protocol.HTTP_1_0)
.message("Unauthorized")
.build();

Assert.assertNull(new JenkinsProxyAuthenticator(null).authenticate(null, response));
}

@Test
public void acceptsBasicChallenge() {
Request previousRequest = new Request.Builder()
.url("https://example.com")
.build();

Response response = new Response.Builder()
.request(previousRequest)
.header("Proxy-Authenticate", "Basic")
.code(407)
.protocol(Protocol.HTTP_1_0)
.message("Unauthorized")
.build();

ProxyConfiguration proxyConfiguration = new ProxyConfiguration("proxy", 80, "user", "password");
String credentials = Credentials.basic("user", "password");
Request requestWithBasicAuth = new JenkinsProxyAuthenticator(proxyConfiguration).authenticate(null, response);

Assert.assertEquals(requestWithBasicAuth.header("Proxy-Authorization"), credentials);
}

@Test
public void refusesAnyChallengeWhichIsNotBasicAuthentication() {
Request previousRequest = new Request.Builder()
.url("https://example.com")
.build();

Response response = new Response.Builder()
.request(previousRequest)
.code(407)
.protocol(Protocol.HTTP_1_0)
.header("Proxy-Authenticate", "Digest")
.message("Unauthorized")
.build();


Assert.assertNull(new JenkinsProxyAuthenticator(null).authenticate(null, response));
}

}