Skip to content

Commit

Permalink
Merge pull request #369 from timja/autofresh-auth
Browse files Browse the repository at this point in the history
JENKINS-64601 Switch to github-api auto-refresh auth tokens
  • Loading branch information
bitwiseman authored Jan 21, 2021
2 parents 95b7b1d + a87d617 commit bde7b1f
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 292 deletions.
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>github-api</artifactId>
<version>1.116</version>
<version>1.122</version>
</dependency>
<dependency>
<groupId>com.coravy.hudson.plugins.github</groupId>
Expand All @@ -53,7 +53,7 @@
<dependency>
<groupId>io.jenkins.plugins</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2-3.3ae26ccdac4e</version>
<version>0.11.2-5.143e44951c52</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,11 @@
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.github.RateLimitHandler;
import org.kohsuke.github.authorization.ImmutableAuthorizationProvider;
import org.kohsuke.github.extras.okhttp3.OkHttpConnector;

import static java.util.logging.Level.FINE;
import static java.util.logging.Level.WARNING;
import static org.apache.commons.lang3.StringUtils.isBlank;

/**
* Utilities that could perhaps be moved into {@code github-api}.
Expand Down Expand Up @@ -340,15 +340,21 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta
String apiUrl = Util.fixEmptyAndTrim(apiUri);
apiUrl = apiUrl != null ? apiUrl : GitHubServerConfig.GITHUB_URL;
String username;
String password;
String password = null;
String hash;
String authHash;
GitHubAppCredentials gitHubAppCredentials = null;
Jenkins jenkins = Jenkins.get();
if (credentials == null) {
username = null;
password = null;
hash = "anonymous";
authHash = "anonymous";
} else if (credentials instanceof GitHubAppCredentials) {
gitHubAppCredentials = (GitHubAppCredentials) credentials;
hash = Util.getDigestOf(gitHubAppCredentials.getAppID() + gitHubAppCredentials.getOwner() + gitHubAppCredentials.getPrivateKey().getPlainText() + SALT); // want to ensure pooling by credential
authHash = Util.getDigestOf(gitHubAppCredentials.getAppID() + "::" + gitHubAppCredentials.getOwner() + "::" + gitHubAppCredentials.getPrivateKey().getPlainText() + "::" + jenkins.getLegacyInstanceId());
username = gitHubAppCredentials.getUsername();
} else if (credentials instanceof StandardUsernamePasswordCredentials) {
StandardUsernamePasswordCredentials c = (StandardUsernamePasswordCredentials) credentials;
username = c.getUsername();
Expand All @@ -369,10 +375,15 @@ public static void checkApiUrlValidity(@Nonnull GitHub gitHub, @CheckForNull Sta

GitHubBuilder gb = createGitHubBuilder(apiUrl, cache);

if (username != null) {
gb.withPassword(username, password);
if (gitHubAppCredentials != null) {
gb.withAuthorizationProvider(gitHubAppCredentials.getAuthorizationProvider());
} else if (username != null && password != null) {
// At the time of this change this works for OAuth tokens as well.
// This may not continue to work in the future, as GitHub has deprecated Login/Password credentials.
gb.withAuthorizationProvider(ImmutableAuthorizationProvider.fromLoginAndPassword(username, password));
}


record = GitHubConnection.connect(connectionId, gb.build(), cache, credentials instanceof GitHubAppCredentials);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import hudson.util.Secret;
import java.io.IOException;
import java.io.Serializable;
import java.security.GeneralSecurityException;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
Expand All @@ -32,13 +33,14 @@
import org.kohsuke.github.GHAppInstallation;
import org.kohsuke.github.GHAppInstallationToken;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.authorization.AuthorizationProvider;
import org.kohsuke.github.extras.authorization.JWTTokenProvider;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.verb.POST;

import static org.jenkinsci.plugins.github_branch_source.GitHubSCMNavigator.DescriptorImpl.getPossibleApiUriItems;
import static org.jenkinsci.plugins.github_branch_source.JwtHelper.createJWT;

@SuppressFBWarnings(value = "SE_NO_SERIALVERSIONID", justification = "XStream")
public class GitHubAppCredentials extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {
Expand Down Expand Up @@ -122,16 +124,45 @@ public void setOwner(String owner) {
this.owner = Util.fixEmpty(owner);
}

@SuppressWarnings("deprecation")
AuthorizationProvider getAuthorizationProvider() {
return new TokenProvider(this);
}

private static AuthorizationProvider createJwtProvider(String appId, String appPrivateKey) {
try {
return new JWTTokenProvider(appId, appPrivateKey);
} catch (GeneralSecurityException e) {
throw new IllegalArgumentException("Couldn't parse private key for GitHub app, make sure it's PKCS#8 format", e);
}
}

private static class TokenProvider extends GitHub.DependentAuthorizationProvider {
private final GitHubAppCredentials credentials;

TokenProvider(GitHubAppCredentials credentials) {
super(createJwtProvider(credentials.appID, credentials.privateKey.getPlainText()));
this.credentials = credentials;
}

public String getEncodedAuthorization() throws IOException {
Secret token = credentials.getPassword(gitHub());
return String.format("token %s", token.getPlainText());
}
}

@SuppressWarnings("deprecation") // preview features are required for GitHub app integration, GitHub api adds deprecated to all preview methods
static AppInstallationToken generateAppInstallationToken(String appId, String appPrivateKey, String apiUrl, String owner) {
static AppInstallationToken generateAppInstallationToken(GitHub gitHubApp, String appId, String appPrivateKey, String apiUrl, String owner) {
JenkinsJVM.checkJenkinsJVM();
// We expect this to be fast but if anything hangs in here we do not want to block indefinitely
try (Timeout timeout = Timeout.limit(30, TimeUnit.SECONDS)) {
String jwtToken = createJWT(appId, appPrivateKey);
GitHub gitHubApp = Connector
.createGitHubBuilder(apiUrl)
.withJwtToken(jwtToken)
.build();

try (Timeout ignored = Timeout.limit(30, TimeUnit.SECONDS)) {
if (gitHubApp == null) {
gitHubApp = Connector
.createGitHubBuilder(apiUrl)
.withAuthorizationProvider(createJwtProvider(appId, appPrivateKey))
.build();
}

GHApp app;
try {
Expand All @@ -151,7 +182,8 @@ static AppInstallationToken generateAppInstallationToken(String appId, String ap
appInstallation = appInstallations.stream()
.filter(installation -> installation.getAccount().getLogin().equals(owner))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(String.format(ERROR_NOT_INSTALLED, appId)));
.orElseThrow(() -> new IllegalArgumentException(String.format(ERROR_NOT_INSTALLED,
appId)));
}

GHAppInstallationToken appInstallationToken = appInstallation
Expand Down Expand Up @@ -198,17 +230,14 @@ private static long getExpirationSeconds(GHAppInstallationToken appInstallationT
return Util.fixEmpty(apiUri) == null ? "https://api.github.com" : apiUri;
}

/**
* {@inheritDoc}
*/
@NonNull
@Override
public Secret getPassword() {
private Secret getPassword(GitHub gitHub) {
synchronized (this) {
try {
if (cachedToken == null || cachedToken.isStale()) {
LOGGER.log(Level.FINE, "Generating App Installation Token for app ID {0}", appID);
cachedToken = generateAppInstallationToken(appID,
cachedToken = generateAppInstallationToken(
gitHub,
appID,
privateKey.getPlainText(),
actualApiUri(),
owner);
Expand All @@ -230,7 +259,15 @@ public Secret getPassword() {

return cachedToken.getToken();
}
}

/**
* {@inheritDoc}
*/
@NonNull
@Override
public Secret getPassword() {
return this.getPassword(null);
}

/**
Expand Down Expand Up @@ -442,7 +479,7 @@ public Secret getPassword() {
// while only slightly increasing the chance that tokens will expire while in use.
LOGGER.log(Level.WARNING,
"Failed to generate new GitHub App Installation Token for app ID " + appID + " on agent: cached token is stale but has not expired");
// Logging the exception here caused a security exeception when trying to read the agent logs during testing
// Logging the exception here caused a security exception when trying to read the agent logs during testing
// Added the exception to a secondary log message that can be viewed if it is needed
LOGGER.log(Level.FINER, () -> Functions.printThrowable(e));
} else {
Expand Down Expand Up @@ -473,6 +510,7 @@ public AppInstallationToken call() throws RuntimeException {
JSONObject fields = JSONObject.fromObject(Secret.fromString(data).getPlainText());
LOGGER.log(Level.FINE, "Generating App Installation Token for app ID {0} for agent", fields.get("appID"));
AppInstallationToken token = generateAppInstallationToken(
null,
(String)fields.get("appID"),
(String)fields.get("privateKey"),
(String)fields.get("apiUri"),
Expand Down

This file was deleted.

Loading

0 comments on commit bde7b1f

Please sign in to comment.