diff --git a/pom.xml b/pom.xml
index 53813d25b..498aea8b1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,7 +39,7 @@
org.jenkins-ci.plugins
github-api
- 1.116
+ 1.122
com.coravy.hudson.plugins.github
@@ -53,7 +53,7 @@
io.jenkins.plugins
jjwt-api
- 0.11.2-3.3ae26ccdac4e
+ 0.11.2-5.143e44951c52
org.jenkins-ci.plugins
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
index a90e607e2..fb9220096 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/Connector.java
@@ -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}.
@@ -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();
@@ -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);
}
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java
index cfba911ac..da10b6a44 100644
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java
+++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java
@@ -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;
@@ -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 {
@@ -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 {
@@ -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
@@ -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);
@@ -230,7 +259,15 @@ public Secret getPassword() {
return cachedToken.getToken();
}
+ }
+ /**
+ * {@inheritDoc}
+ */
+ @NonNull
+ @Override
+ public Secret getPassword() {
+ return this.getPassword(null);
}
/**
@@ -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 {
@@ -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"),
diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/JwtHelper.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/JwtHelper.java
deleted file mode 100644
index 53cf893b9..000000000
--- a/src/main/java/org/jenkinsci/plugins/github_branch_source/JwtHelper.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package org.jenkinsci.plugins.github_branch_source;
-
-import io.jsonwebtoken.JwtBuilder;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
-import java.security.GeneralSecurityException;
-import java.security.Key;
-import java.security.KeyFactory;
-import java.security.PrivateKey;
-import java.security.spec.PKCS8EncodedKeySpec;
-import java.util.Base64;
-import java.util.Date;
-
-import static java.util.Objects.requireNonNull;
-import java.util.concurrent.TimeUnit;
-
-class JwtHelper {
-
- /**
- * Somewhat less than the maximum JWT validity.
- */
- static final long VALIDITY_MS = TimeUnit.MINUTES.toMillis(8);
-
- /**
- * Create a JWT for authenticating to GitHub as an app installation
- * @param githubAppId the app ID
- * @param privateKey PKC#8 formatted private key
- * @return JWT for authenticating to GitHub
- */
- static String createJWT(String githubAppId, final String privateKey) {
- requireNonNull(githubAppId, privateKey);
-
- SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RS256;
-
- long nowMillis = System.currentTimeMillis();
- Date now = new Date(nowMillis);
-
- Key signingKey;
- try {
- signingKey = getPrivateKeyFromString(privateKey);
- } catch (GeneralSecurityException e) {
- throw new IllegalArgumentException("Couldn't parse private key for GitHub app, make sure it's PKCS#8 format", e);
- }
-
- JwtBuilder builder = Jwts.builder()
- .setIssuedAt(now)
- .setIssuer(githubAppId)
- .signWith(signingKey, signatureAlgorithm);
-
- Date exp = new Date(nowMillis + VALIDITY_MS);
- builder.setExpiration(exp);
-
- return builder.compact();
- }
-
- /**
- * Convert a PKCS#8 formatted private key in string format into a java PrivateKey
- * @param key PCKS#8 string
- * @return private key
- * @throws GeneralSecurityException if we couldn't parse the string
- */
- private static PrivateKey getPrivateKeyFromString(final String key) throws GeneralSecurityException {
- if (key.contains(" RSA ")) {
- throw new InvalidPrivateKeyException(
- "Private key must be a PKCS#8 formatted string, to convert it from PKCS#1 use: "
- + "openssl pkcs8 -topk8 -inform PEM -outform PEM -in current-key.pem -out new-key.pem -nocrypt"
- );
- }
-
- String privateKeyContent = key
- .replaceAll("\\n", "")
- .replaceAll("\\r", "")
- .replace("-----BEGIN PRIVATE KEY-----", "")
- .replace("-----END PRIVATE KEY-----", "");
-
- KeyFactory kf = KeyFactory.getInstance("RSA");
-
- try {
- byte[] decode = Base64.getDecoder().decode(privateKeyContent);
- PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(decode);
-
- return kf.generatePrivate(keySpecPKCS8);
- } catch (IllegalArgumentException e) {
- throw new InvalidPrivateKeyException("Failed to decode private key: " + e.getMessage());
- }
- }
-
-}
diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java
index a7d930bd3..23a0afb7a 100644
--- a/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java
+++ b/src/test/java/org/jenkinsci/plugins/github_branch_source/GithubAppCredentialsTest.java
@@ -20,6 +20,8 @@
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
+import org.kohsuke.github.GitHub;
+import org.kohsuke.github.authorization.AuthorizationProvider;
import java.time.Duration;
import java.time.Instant;
@@ -37,6 +39,7 @@
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.hamcrest.Matchers.*;
+import static org.jenkinsci.plugins.github_branch_source.Connector.createGitHubBuilder;
public class GithubAppCredentialsTest extends AbstractGitHubWireMockTest {
@@ -46,6 +49,38 @@ public class GithubAppCredentialsTest extends AbstractGitHubWireMockTest {
private static GitHubAppCredentials appCredentials;
private static LogRecorder logRecorder;
+ // https://stackoverflow.com/a/22176759/4951015
+ public static final String PKCS8_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" +
+ // Windows line ending
+ "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD7vHsVwyDV8cj7\r\n" +
+ // This should also work
+ "5yR4WWl6rlgf/e5zmeBgtm0PCgnitcSbD5FU33301DPY5a7AtqVBOwEnE14L9XS7\r" +
+ "ov61U+x1m4aQmqR/dPQaA2ayh2cYPszWNQMp42ArDIfg7DhSrvsRJKHsbPXlPjqe\n" +
+ "c0udLqhSLVIO9frNLf+dAsLsgYk8O39PKGb33akGG7tWTe0J+akNQjgbS7vOi8sS\n" +
+ "NLwHIdYfz/Am+6Xmm+J4yVs6+Xt3kOeLdFBkz8H/HGsJq854MbIAK/HuId1MOPS0\n" +
+ "cDWh37tzRsM+q/HZzYRkc5bhNKw/Mj9jN9jD5GH0Lfea0QFedjppf1KvWdcXn+/W\n" +
+ "M7OmyfhvAgMBAAECggEAN96H7reExRbJRWbySCeH6mthMZB46H0hODWklK7krMUs\n" +
+ "okFdPtnvKXQjIaMwGqMuoACJa/O3bq4GP1KYdwPuOdfPkK5RjdwWBOP2We8FKXNe\n" +
+ "oLfZQOWuxT8dtQSYJ3mgTRi1OzSfikY6Wko6YOMnBj36tUlQZVMtJNqlCjphi9Uz\n" +
+ "6EyvRURlDG8sBBbC7ods5B0789qk3iGH/97ia+1QIqXAUaVFg3/BA6wkxkbNG2sN\n" +
+ "tqULgVYTw32Oj/Y/H1Y250RoocTyfsUS3I3aPIlnvcgp2bugWqDyYJ58nDIt3Pku\n" +
+ "fjImWrNz/pNiEs+efnb0QEk7m5hYwxmyXN4KRSv0OQKBgQD+I3Y3iNKSVr6wXjur\n" +
+ "OPp45fxS2sEf5FyFYOn3u760sdJOH9fGlmf9sDozJ8Y8KCaQCN5tSe3OM+XDrmiw\n" +
+ "Cu/oaqJ1+G4RG+6w1RJF+5Nfg6PkUs7eJehUgZ2Tox8Tg1mfVIV8KbMwNi5tXpug\n" +
+ "MVmA2k9xjc4uMd2jSnSj9NAqrQKBgQD9lIO1tY6YKF0Eb0Qi/iLN4UqBdJfnALBR\n" +
+ "MjxYxqqI8G4wZEoZEJJvT1Lm6Q3o577N95SihZoj69tb10vvbEz1pb3df7c1HEku\n" +
+ "LXcyVMvjR/CZ7dOSNgLGAkFfOoPhcF/OjSm4DrGPe3GiBxhwXTBjwJ5TIgEDkVIx\n" +
+ "ZVo5r7gPCwKBgQCOvsZo/Q4hql2jXNqxGuj9PVkUBNFTI4agWEYyox7ECdlxjks5\n" +
+ "vUOd5/1YvG+JXJgEcSbWRh8volDdL7qXnx0P881a6/aO35ybcKK58kvd62gEGEsf\n" +
+ "1jUAOmmTAp2y7SVK7EOp8RY370b2oZxSR0XZrUXQJ3F22wV98ZVAfoLqZQKBgDIr\n" +
+ "PdunbezAn5aPBOX/bZdZ6UmvbZYwVrHZxIKz2214U/STAu3uj2oiQX6ZwTzBDMjn\n" +
+ "IKr+z74nnaCP+eAGhztabTPzXqXNUNUn/Zshl60BwKJToTYeJXJTY+eZRhpGB05w\n" +
+ "Mz7M+Wgvvg2WZcllRnuV0j0UTysLhz1qle0vzLR9AoGBAOukkFFm2RLm9N1P3gI8\n" +
+ "mUadeAlYRZ5o0MvumOHaB5pDOCKhrqAhop2gnM0f5uSlapCtlhj0Js7ZyS3Giezg\n" +
+ "38oqAhAYxy2LMoLD7UtsHXNp0OnZ22djcDwh+Wp2YORm7h71yOM0NsYubGbp+CmT\n" +
+ "Nw9bewRvqjySBlDJ9/aNSeEY\n" +
+ "-----END PRIVATE KEY-----";
+
@Rule
public GitSampleRepoRule sampleRepo = new GitSampleRepoRule();
@@ -54,7 +89,7 @@ public static void setUpJenkins() throws Exception {
//Add credential (Must have valid private key for Jwt to work, but App doesn't have to actually exist)
store = CredentialsProvider.lookupStores(r.jenkins).iterator().next();
appCredentials = new GitHubAppCredentials(
- CredentialsScope.GLOBAL, myAppCredentialsId, "sample", "54321", Secret.fromString(JwtHelperTest.PKCS8_PRIVATE_KEY));
+ CredentialsScope.GLOBAL, myAppCredentialsId, "sample", "54321", Secret.fromString(PKCS8_PRIVATE_KEY));
appCredentials.setOwner("cloudbeers");
store.addCredentials(Domain.global(), appCredentials);
@@ -209,6 +244,66 @@ public void setUpWireMock() throws Exception {
"}")));
}
+ @Test
+ public void testProviderRefresh() throws Exception {
+ long notStaleSeconds = GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS;
+ try {
+ appCredentials.setApiUri(githubApi.baseUrl());
+
+ // We want to demonstrate successful caching without waiting for the default 1 minute
+ // Must set this to a large enough number to avoid flaky test
+ GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS = 5;
+
+ // Ensure we are working from sufficiently clean cache state
+ Thread.sleep(Duration.ofSeconds(GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS + 2).toMillis());
+
+ AuthorizationProvider provider = appCredentials.getAuthorizationProvider();
+ GitHub githubInstance = createGitHubBuilder(githubApi.baseUrl())
+ .withAuthorizationProvider(provider).build();
+
+ // First Checkout on controller should use cached
+ provider.getEncodedAuthorization();
+ // Multiple checkouts in quick succession should use cached token
+ provider.getEncodedAuthorization();
+ Thread.sleep(Duration.ofSeconds(GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS + 2).toMillis());
+ // Checkout after token is stale refreshes - fallback due to unexpired token
+ provider.getEncodedAuthorization();
+ // Checkout after error will refresh again on controller - new token expired but not stale
+ provider.getEncodedAuthorization();
+ Thread.sleep(Duration.ofSeconds(GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS + 2).toMillis());
+ // Checkout after token is stale refreshes - error on controller is not catastrophic
+ provider.getEncodedAuthorization();
+ // Checkout after error will refresh again on controller - new token expired but not stale
+ provider.getEncodedAuthorization();
+ // Multiple checkouts in quick succession should use cached token
+ provider.getEncodedAuthorization();
+
+ List credentialsLog = getOutputLines();
+
+ //Verify correct messages from GitHubAppCredential logger indicating token was retrieved on agent
+ assertThat("Creds should cache on master",
+ credentialsLog, contains(
+ // refresh on controller
+ "Generating App Installation Token for app ID 54321",
+ // next call uses cached token
+ // sleep and then refresh stale token
+ "Generating App Installation Token for app ID 54321",
+ // next call (error forced by wiremock)
+ "Failed to generate new GitHub App Installation Token for app ID 54321: cached token is stale but has not expired",
+ // next call refreshes the still stale token
+ "Generating App Installation Token for app ID 54321",
+ // sleep and then refresh stale token hits another error forced by wiremock
+ "Failed to generate new GitHub App Installation Token for app ID 54321: cached token is stale but has not expired",
+ // next call refreshes the still stale token
+ "Generating App Installation Token for app ID 54321"
+ // next call uses cached token
+ ));
+ } finally {
+ GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS = notStaleSeconds;
+ logRecorder.doClear();
+ }
+ }
+
@Test
public void testAgentRefresh() throws Exception {
long notStaleSeconds = GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS;
@@ -219,6 +314,9 @@ public void testAgentRefresh() throws Exception {
// Must set this to a large enough number to avoid flaky test
GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS = 5;
+ // Ensure we are working from sufficiently clean cache state
+ Thread.sleep(Duration.ofSeconds(GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS + 2).toMillis());
+
final String gitCheckoutStep = String.format(
" git url: REPO, credentialsId: '%s'",
myAppCredentialsId);
@@ -261,7 +359,6 @@ public void testAgentRefresh() throws Exception {
r.waitUntilNoActivity();
System.out.println(JenkinsRule.getLog(run));
- assertThat(run.getResult(), equalTo(Result.SUCCESS));
List credentialsLog = getOutputLines();
@@ -300,6 +397,10 @@ credentialsLog, contains(
// checkout scm
// (No token generation)
));
+
+ // Check success after output. Output will be more informative if something goes wrong.
+ assertThat(run.getResult(), equalTo(Result.SUCCESS));
+
} finally {
GitHubAppCredentials.AppInstallationToken.NOT_STALE_MINIMUM_SECONDS = notStaleSeconds;
logRecorder.doClear();
@@ -324,4 +425,4 @@ static String printDate(Date dt) {
ChronoUnit.SECONDS));
}
-}
\ No newline at end of file
+}
diff --git a/src/test/java/org/jenkinsci/plugins/github_branch_source/JwtHelperTest.java b/src/test/java/org/jenkinsci/plugins/github_branch_source/JwtHelperTest.java
deleted file mode 100644
index d63381dda..000000000
--- a/src/test/java/org/jenkinsci/plugins/github_branch_source/JwtHelperTest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-package org.jenkinsci.plugins.github_branch_source;
-
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jws;
-import io.jsonwebtoken.Jwts;
-import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
-import java.security.PublicKey;
-import java.security.spec.X509EncodedKeySpec;
-import java.util.Base64;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.Is.is;
-import static org.jenkinsci.plugins.github_branch_source.JwtHelper.createJWT;
-import static org.mockito.ArgumentMatchers.contains;
-
-public class JwtHelperTest {
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- // https://stackoverflow.com/a/22176759/4951015
- public static final String PKCS8_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" +
- // Windows line ending
- "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD7vHsVwyDV8cj7\r\n" +
- // This should also work
- "5yR4WWl6rlgf/e5zmeBgtm0PCgnitcSbD5FU33301DPY5a7AtqVBOwEnE14L9XS7\r" +
- "ov61U+x1m4aQmqR/dPQaA2ayh2cYPszWNQMp42ArDIfg7DhSrvsRJKHsbPXlPjqe\n" +
- "c0udLqhSLVIO9frNLf+dAsLsgYk8O39PKGb33akGG7tWTe0J+akNQjgbS7vOi8sS\n" +
- "NLwHIdYfz/Am+6Xmm+J4yVs6+Xt3kOeLdFBkz8H/HGsJq854MbIAK/HuId1MOPS0\n" +
- "cDWh37tzRsM+q/HZzYRkc5bhNKw/Mj9jN9jD5GH0Lfea0QFedjppf1KvWdcXn+/W\n" +
- "M7OmyfhvAgMBAAECggEAN96H7reExRbJRWbySCeH6mthMZB46H0hODWklK7krMUs\n" +
- "okFdPtnvKXQjIaMwGqMuoACJa/O3bq4GP1KYdwPuOdfPkK5RjdwWBOP2We8FKXNe\n" +
- "oLfZQOWuxT8dtQSYJ3mgTRi1OzSfikY6Wko6YOMnBj36tUlQZVMtJNqlCjphi9Uz\n" +
- "6EyvRURlDG8sBBbC7ods5B0789qk3iGH/97ia+1QIqXAUaVFg3/BA6wkxkbNG2sN\n" +
- "tqULgVYTw32Oj/Y/H1Y250RoocTyfsUS3I3aPIlnvcgp2bugWqDyYJ58nDIt3Pku\n" +
- "fjImWrNz/pNiEs+efnb0QEk7m5hYwxmyXN4KRSv0OQKBgQD+I3Y3iNKSVr6wXjur\n" +
- "OPp45fxS2sEf5FyFYOn3u760sdJOH9fGlmf9sDozJ8Y8KCaQCN5tSe3OM+XDrmiw\n" +
- "Cu/oaqJ1+G4RG+6w1RJF+5Nfg6PkUs7eJehUgZ2Tox8Tg1mfVIV8KbMwNi5tXpug\n" +
- "MVmA2k9xjc4uMd2jSnSj9NAqrQKBgQD9lIO1tY6YKF0Eb0Qi/iLN4UqBdJfnALBR\n" +
- "MjxYxqqI8G4wZEoZEJJvT1Lm6Q3o577N95SihZoj69tb10vvbEz1pb3df7c1HEku\n" +
- "LXcyVMvjR/CZ7dOSNgLGAkFfOoPhcF/OjSm4DrGPe3GiBxhwXTBjwJ5TIgEDkVIx\n" +
- "ZVo5r7gPCwKBgQCOvsZo/Q4hql2jXNqxGuj9PVkUBNFTI4agWEYyox7ECdlxjks5\n" +
- "vUOd5/1YvG+JXJgEcSbWRh8volDdL7qXnx0P881a6/aO35ybcKK58kvd62gEGEsf\n" +
- "1jUAOmmTAp2y7SVK7EOp8RY370b2oZxSR0XZrUXQJ3F22wV98ZVAfoLqZQKBgDIr\n" +
- "PdunbezAn5aPBOX/bZdZ6UmvbZYwVrHZxIKz2214U/STAu3uj2oiQX6ZwTzBDMjn\n" +
- "IKr+z74nnaCP+eAGhztabTPzXqXNUNUn/Zshl60BwKJToTYeJXJTY+eZRhpGB05w\n" +
- "Mz7M+Wgvvg2WZcllRnuV0j0UTysLhz1qle0vzLR9AoGBAOukkFFm2RLm9N1P3gI8\n" +
- "mUadeAlYRZ5o0MvumOHaB5pDOCKhrqAhop2gnM0f5uSlapCtlhj0Js7ZyS3Giezg\n" +
- "38oqAhAYxy2LMoLD7UtsHXNp0OnZ22djcDwh+Wp2YORm7h71yOM0NsYubGbp+CmT\n" +
- "Nw9bewRvqjySBlDJ9/aNSeEY\n" +
- "-----END PRIVATE KEY-----";
-
- public static final String PKCS8_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" +
- "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+7x7FcMg1fHI++ckeFlp\n" +
- "eq5YH/3uc5ngYLZtDwoJ4rXEmw+RVN999NQz2OWuwLalQTsBJxNeC/V0u6L+tVPs\n" +
- "dZuGkJqkf3T0GgNmsodnGD7M1jUDKeNgKwyH4Ow4Uq77ESSh7Gz15T46nnNLnS6o\n" +
- "Ui1SDvX6zS3/nQLC7IGJPDt/Tyhm992pBhu7Vk3tCfmpDUI4G0u7zovLEjS8ByHW\n" +
- "H8/wJvul5pvieMlbOvl7d5Dni3RQZM/B/xxrCavOeDGyACvx7iHdTDj0tHA1od+7\n" +
- "c0bDPqvx2c2EZHOW4TSsPzI/YzfYw+Rh9C33mtEBXnY6aX9Sr1nXF5/v1jOzpsn4\n" +
- "bwIDAQAB\n" +
- "-----END PUBLIC KEY-----";
-
-
- private static final String PKCS1_PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n" +
- "MIIEpAIBAAKCAQEA26y2ZLYaNKHYg1FehH/WmXZ+SXG9ofLCf7+tR0j/BHbQy1Ck\n" +
- "u6Pqxn10nKPrAZSFakNDKI1vf92+Ny8LFitBucs2JaDSm1kUHjZaoCbp2FQmbr28\n" +
- "eO+q0oIaJ67WaIF9o1DzCiBBgqCOqZpDdZY1peRPQ7ttBfBvPOi9zEiWplrn2IlL\n" +
- "tlndlYtV+KHlIy7odaCaSHjzawTBxLe82lpX5+YHy0doNlI5l/epJMtjcE/l2jEj\n" +
- "xMZxWz4ZAiXd8hLYonUzxaup8IMKm4K8eh++4UcXAs0tjA0CGaieeyQZyBLPwFyf\n" +
- "k3JStqbBgwaKLzV0D1ayokQNvc0cm4tdgk6gVwIDAQABAoIBAGlZzSdDhhHTxIhF\n" +
- "z7RvsrVqdGo4mB9A0zJ89FcJlPPJH51CEZ7Dn+aNaA1vN1dMqScrFtwt6FlEOOMy\n" +
- "NnjtSdoWsOMe26IQ+Gr82j2QK/nJcZ0OdYLyPdQy/OQnH0CDSYO3YLdsfL5uzbxc\n" +
- "9RlBbn0enzz2d/SvOEnXvJ5p+YXRk3Y8Toccu66nPUKkeWDzZ3Ql/mf2Piw1VwvF\n" +
- "/5pvZRiH5Lh5MCc7AxHlDFXRq5jQKxSdJrtHhB/GFRfHg6EOAKfCGbPHwYIMb5BW\n" +
- "KNxRRyfpAPhUP9a+GgH4mHXkv+wSR87zE3hbCf7Fg/4mB25Cx4r/34E5W0F0XuCN\n" +
- "HzSwXHECgYEA8pdeT6R2mlWDgD7IfhyeYoUcJ0oXvd6dKlGOETlkzkGi4QvP3BsM\n" +
- "wg0sELPhuYCOG53SzSW9d5QkqDYJRY4/xg15QV2LYOMpP5b9cjJZRE3Uo9BVIBum\n" +
- "EFVZvuGzZaFUO6Zx3xQiQgHuCP8Tx676vTk36ka3fVQV5FdY8tP0HyUCgYEA59ET\n" +
- "v6eE2s10T9JeO3htK5TjwioMYpp3j+HUZX78anyqWw17OUityWi/dRnCoyfpPuIi\n" +
- "qBGNjMk3JZYz3MmoR9pPGKgzI43EQKBay6+CjZfcQ4Vw7qzW0bUKD2xfLU+ZOeR+\n" +
- "jJn5wdBvZHooX8e1en/aLj5h9h9FzhAy3/Sd1ssCgYB1S8tGJvdR2FclAzZeA+hx\n" +
- "KntaY/Dm1WSYuaY/ncioEgR3XAa9Hjck/Ml5qgBSeV487CqpFr5tuyueScJh503e\n" +
- "rVUbzec+iZfAL3mMZdvTsu5F5s3CIJxC+YHTUb40PbVEwk381vdZgyVdJDikLG8A\n" +
- "X1Ix7M97wdRz++f+QY2gIQKBgQCeHaiHt95RU4O7EjT+AVUNPd/fxsht1QgqFpHF\n" +
- "rMjEZUXZFyfuWZlX4F9+otR0bruUDbAvzNEsru4zb/Dt7ooegFQk8Ez5OjAbGIT1\n" +
- "mz/EDknJsFHoKfHYVdCH1pZQlJNhvm1mv3twbBgeg4fYVKJ+7IfHtPsiYhA9ziS1\n" +
- "RucF4wKBgQDJfd1BxBdkeSRIJ/C75iZ4vWWsM/JvMI1L68ZJEWdTqUvyyy9xLWEe\n" +
- "8wIGZTv/mnuQhOGSaUUk0fTup7ZwTfmg+hahhCBe5kSh4bav5+knu6yQ7nhwccs8\n" +
- "WXeajzno43UHZksae1LP1B3J1+0adxpykCMzWl19XZkxtVkYVi0Q3g==\n" +
- "-----END RSA PRIVATE KEY-----";
-
- private static final String PKCS8_PRIVATE_KEY_CONTAINING_RSA_CHARS = "-----BEGIN PRIVATE KEY-----\n"
- + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGKsPHpCMqBVUw\n"
- + "BBxD33j/WShTVB0vdmu+SqgZeNULSy0+ipBaDRgTbyhB0mA2l9M24mH7B/vY0Cjn\n"
- + "rN+42bV92fMFQRCvxgQ9cRsi2bgL4XOgSdJB9cH/GWev04kUqWRihjm5quPzp62z\n"
- + "RPmNoSbrbLWVtjcfAYOSDA1SP4XBdRaWW8CjQU7VB73RGacJp7mODvi1gqccsWK6\n"
- + "Qiu6fbGjTeimrWTnEPFFGdsvKgLrGqKyZBGBj9uKKC5o+l4HXvSeKSUrYJbt0bWs\n"
- + "uOeirDqQ2y2QY1OxoiXGH2N5FVIJ/opVyOq/LyGgVoOSqjUzmRW2DuZT7LQm/Gl9\n"
- + "EmhuEUBfAgMBAAECggEBAJWRx41SpLvdpIuGPrM348KPT7F9Rj4BmpbZEIGRQvOw\n"
- + "PSj8OrHNOkPI3VC48aei9mdxfNSVFRBzJLygLYf+wk6IBzYLAwY4ZhDd4sZuH8zP\n"
- + "0I7FyS3ByTe6vBjoh4mRxNPcTYuGoWDRSXiKcfTlElQVDAVAr9/2K5E7CX7vtQvq\n"
- + "+R3Pi+5HGv88FxbRn7uh4PQDUBvArvJdLZVSLiq2EBkmwHGa4DZSoqm5mG8Y5W1B\n"
- + "usDU5YSQhqd0t7RPeOh7VV7p0mnF9Pn0Kmc3e182N05spOzhOmP8nm9b9eGecPWE\n"
- + "fqqdmWBuellCHTe0VGUBXHUCUFcItt/6xaZvwaSzJzkCgYEA5YsgIJkiy7sxqqTD\n"
- + "QZLmfS4Zr2VCLlEtdBaJG6FIrKkq7YJ3njL0CyFQ4qT7Nba1uiLYL6wH3O2MaVvD\n"
- + "JqRJbWfJjhcmNGnsMesFAZid12shLq5oB1Ptg2VRnNcTzdLY30MF5JIHA8YUV8/s\n"
- + "xWl5PQ/L08rgHILNMpndSHee8LsCgYEA3QHaCM+N78GV03r1Rw1S9ggcJHmNa8C2\n"
- + "tUSVzkIE2RgOZ+PJCt9ij0OYxD6DDdLZDd9OUw2tvMqZoag2TeprP6qtu0HKSEQj\n"
- + "7YjA5BMAWqqyn8RXada0swr/y3ARKe+1haZKKzS/7qeNjD8GGTQp+NvtOgQ5DfJl\n"
- + "hDDolKOhlq0CgYEAnsPQr9tbXtCV9LJLPwKtGy4Uo+UElmadaqrfoFW4n3vObkKM\n"
- + "G8agV0Zu3KRCAI/kN9876hUxxxQixwip/QMqqlpb5USLrzsIHCqy5ry5h7LYW6JT\n"
- + "36WkJPqiLTnxv62zRRDldYevBGQv0+DDonNmYN6ZG186DV5HMVWM4T+jllsCgYAe\n"
- + "iEj0+qejPd1TECOeo0qYztoEd/5/qmoTdNw1WI2O6HHlDGUT6XSWUkJiqjg0yrJN\n"
- + "5lHNy4/7CwpaeQC3lvEmJJBH1Hj7rt4/zKrJV46u9/IhfGCPMKhaK+TW2C6m2oT7\n"
- + "Z9PLUEhL0j4N6A8RoFFEHi4R289+C8TWlGMtVcXXKQKBgEd1zPh3okoOEBLWBswc\n"
- + "rYeW6a5YMUlWdjv3AaVGWvXCBVQV/nHLbHePDw8/Lj71+tW+aJP3/RSA38T1EbJc\n"
- + "NbI/fRyHodR+Cf7jCm8DWg/lORzzGSA7FGZyV8M/AW4WQ/NjFuHme4Dg7qULEuk+\n"
- + "CK428WLq7hhesv0p8tsmJP+n\n"
- + "-----END PRIVATE KEY-----";
-
- private static final String PKCS8_PUBLIC_KEY_CONTAINING_RSA_CHARS = "-----BEGIN PUBLIC KEY-----\n"
- + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxirDx6QjKgVVMAQcQ994\n"
- + "/1koU1QdL3ZrvkqoGXjVC0stPoqQWg0YE28oQdJgNpfTNuJh+wf72NAo56zfuNm1\n"
- + "fdnzBUEQr8YEPXEbItm4C+FzoEnSQfXB/xlnr9OJFKlkYoY5uarj86ets0T5jaEm\n"
- + "62y1lbY3HwGDkgwNUj+FwXUWllvAo0FO1Qe90RmnCae5jg74tYKnHLFiukIrun2x\n"
- + "o03opq1k5xDxRRnbLyoC6xqismQRgY/biiguaPpeB170niklK2CW7dG1rLjnoqw6\n"
- + "kNstkGNTsaIlxh9jeRVSCf6KVcjqvy8hoFaDkqo1M5kVtg7mU+y0JvxpfRJobhFA\n"
- + "XwIDAQAB\n"
- + "-----END PUBLIC KEY-----";
-
- @Test
- public void createJWT_is_valid() throws Exception {
- String jwt = createJWT("123", PKCS8_PRIVATE_KEY);
- Jws parsedJwt = Jwts.parser()
- .setSigningKey(getPublicKeyFromString(PKCS8_PUBLIC_KEY))
- .parseClaimsJws(jwt);
- assertThat(parsedJwt.getBody().getIssuer(), is("123"));
- }
-
- @Test
- public void createJWT_with_key_containing_RSA_is_valid() throws Exception {
- String jwt = createJWT("123", PKCS8_PRIVATE_KEY_CONTAINING_RSA_CHARS);
- Jws parsedJwt = Jwts.parser()
- .setSigningKey(getPublicKeyFromString(PKCS8_PUBLIC_KEY_CONTAINING_RSA_CHARS))
- .parseClaimsJws(jwt);
- assertThat(parsedJwt.getBody().getIssuer(), is("123"));
- }
-
- @Test
- public void createJWT_with_pkcs1_is_invalid() {
- expectedException.expect(InvalidPrivateKeyException.class);
- expectedException.expectMessage(contains("openssl pkcs8 -topk8"));
- createJWT("123", PKCS1_PRIVATE_KEY);
- }
-
- @Test
- public void createJWT_with_not_base64_is_invalid() {
- expectedException.expect(InvalidPrivateKeyException.class);
- expectedException.expectMessage(contains("Failed to decode private key"));
- createJWT("123", "d£!@!@£!@£");
- }
-
- private static PublicKey getPublicKeyFromString(final String key) throws GeneralSecurityException {
- String publicKeyContent = key.replaceAll("\\n", "")
- .replace("-----BEGIN PUBLIC KEY-----", "")
- .replace("-----END PUBLIC KEY-----", "");
-
- KeyFactory kf = KeyFactory.getInstance("RSA");
-
- X509EncodedKeySpec keySpecPKCS8 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyContent));
-
- return kf.generatePublic(keySpecPKCS8);
- }
-}