myselfConsumer,
+ AuthorizationProvider authorizationProvider) throws IOException {
super(apiUrl,
- login,
- oauthAccessToken,
- jwtToken,
- password,
connector,
rateLimitHandler,
abuseLimitHandler,
rateLimitChecker,
- myselfConsumer);
+ myselfConsumer,
+ authorizationProvider);
}
@Nonnull
@@ -114,8 +109,12 @@ static HttpURLConnection setupConnection(@Nonnull GitHubClient client, @Nonnull
// if the authentication is needed but no credential is given, try it anyway (so that some calls
// that do work with anonymous access in the reduced form should still work.)
- if (client.encodedAuthorization != null)
- connection.setRequestProperty("Authorization", client.encodedAuthorization);
+ if (!request.headers().containsKey("Authorization")) {
+ String authorization = client.getEncodedAuthorization();
+ if (authorization != null) {
+ connection.setRequestProperty("Authorization", client.getEncodedAuthorization());
+ }
+ }
setRequestMethod(request.method(), connection);
buildRequest(request, connection);
diff --git a/src/main/java/org/kohsuke/github/authorization/AuthorizationProvider.java b/src/main/java/org/kohsuke/github/authorization/AuthorizationProvider.java
new file mode 100644
index 0000000000..4dd615885d
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/authorization/AuthorizationProvider.java
@@ -0,0 +1,43 @@
+package org.kohsuke.github.authorization;
+
+import java.io.IOException;
+
+/**
+ * Provides a functional interface that returns a valid encodedAuthorization. This strategy allows for a provider that
+ * dynamically changes the credentials. Each request will request the credentials from the provider.
+ */
+public interface AuthorizationProvider {
+ /**
+ * An static instance for an ANONYMOUS authorization provider
+ */
+ AuthorizationProvider ANONYMOUS = new AnonymousAuthorizationProvider();
+
+ /**
+ * Returns the credentials to be used with a given request. As an example, a authorization provider for a bearer
+ * token will return something like:
+ *
+ *
+ * {@code
+ * @Override
+ * public String getEncodedAuthorization() {
+ * return "Bearer myBearerToken";
+ * }
+ * }
+ *
+ *
+ * @return encoded authorization string, can be null
+ * @throws IOException
+ * on any error that prevents the provider from getting a valid authorization
+ */
+ String getEncodedAuthorization() throws IOException;
+
+ /**
+ * A {@link AuthorizationProvider} that ensures that no credentials are returned
+ */
+ class AnonymousAuthorizationProvider implements AuthorizationProvider {
+ @Override
+ public String getEncodedAuthorization() throws IOException {
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/authorization/ImmutableAuthorizationProvider.java b/src/main/java/org/kohsuke/github/authorization/ImmutableAuthorizationProvider.java
new file mode 100644
index 0000000000..41a113285a
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/authorization/ImmutableAuthorizationProvider.java
@@ -0,0 +1,123 @@
+package org.kohsuke.github.authorization;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import javax.annotation.CheckForNull;
+
+/**
+ * A {@link AuthorizationProvider} that always returns the same credentials
+ */
+public class ImmutableAuthorizationProvider implements AuthorizationProvider {
+
+ private final String authorization;
+
+ public ImmutableAuthorizationProvider(String authorization) {
+ this.authorization = authorization;
+ }
+
+ /**
+ * Builds and returns a {@link AuthorizationProvider} from a given oauthAccessToken
+ *
+ * @param oauthAccessToken
+ * The token
+ * @return a correctly configured {@link AuthorizationProvider} that will always return the same provided
+ * oauthAccessToken
+ */
+ public static AuthorizationProvider fromOauthToken(String oauthAccessToken) {
+ return new UserProvider(String.format("token %s", oauthAccessToken));
+ }
+
+ /**
+ * Builds and returns a {@link AuthorizationProvider} from a given oauthAccessToken
+ *
+ * @param oauthAccessToken
+ * The token
+ * @param login
+ * The login for this token
+ *
+ * @return a correctly configured {@link AuthorizationProvider} that will always return the same provided
+ * oauthAccessToken
+ */
+ public static AuthorizationProvider fromOauthToken(String oauthAccessToken, String login) {
+ return new UserProvider(String.format("token %s", oauthAccessToken), login);
+ }
+
+ /**
+ * Builds and returns a {@link AuthorizationProvider} from a given App Installation Token
+ *
+ * @param appInstallationToken
+ * A string containing the GitHub App installation token
+ * @return the configured Builder from given GitHub App installation token.
+ */
+ public static AuthorizationProvider fromAppInstallationToken(String appInstallationToken) {
+ return fromOauthToken(appInstallationToken, "");
+ }
+
+ /**
+ * Builds and returns a {@link AuthorizationProvider} from a given jwtToken
+ *
+ * @param jwtToken
+ * The JWT token
+ * @return a correctly configured {@link AuthorizationProvider} that will always return the same provided jwtToken
+ */
+ public static AuthorizationProvider fromJwtToken(String jwtToken) {
+ return new ImmutableAuthorizationProvider(String.format("Bearer %s", jwtToken));
+ }
+
+ /**
+ * Builds and returns a {@link AuthorizationProvider} from the given user/password pair
+ *
+ * @param login
+ * The login for the user, usually the same as the username
+ * @param password
+ * The password for the associated user
+ * @return a correctly configured {@link AuthorizationProvider} that will always return the credentials for the same
+ * user and password combo
+ * @deprecated Login with password credentials are no longer supported by GitHub
+ */
+ @Deprecated
+ public static AuthorizationProvider fromLoginAndPassword(String login, String password) {
+ try {
+ String authorization = (String.format("%s:%s", login, password));
+ String charsetName = StandardCharsets.UTF_8.name();
+ String b64encoded = Base64.getEncoder().encodeToString(authorization.getBytes(charsetName));
+ String encodedAuthorization = String.format("Basic %s", b64encoded);
+ return new UserProvider(encodedAuthorization, login);
+ } catch (UnsupportedEncodingException e) {
+ // If UTF-8 isn't supported, there are bigger problems
+ throw new IllegalStateException("Could not generate encoded authorization", e);
+ }
+ }
+
+ @Override
+ public String getEncodedAuthorization() {
+ return this.authorization;
+ }
+
+ /**
+ * An internal class representing all user-related credentials, which are credentials that have a login or should
+ * query the user endpoint for the login matching this credential.
+ */
+ private static class UserProvider extends ImmutableAuthorizationProvider implements UserAuthorizationProvider {
+
+ private final String login;
+
+ UserProvider(String authorization) {
+ this(authorization, null);
+ }
+
+ UserProvider(String authorization, String login) {
+ super(authorization);
+ this.login = login;
+ }
+
+ @CheckForNull
+ @Override
+ public String getLogin() {
+ return login;
+ }
+
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/authorization/OrgAppInstallationAuthorizationProvider.java b/src/main/java/org/kohsuke/github/authorization/OrgAppInstallationAuthorizationProvider.java
new file mode 100644
index 0000000000..020725fb41
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/authorization/OrgAppInstallationAuthorizationProvider.java
@@ -0,0 +1,63 @@
+package org.kohsuke.github.authorization;
+
+import org.kohsuke.github.BetaApi;
+import org.kohsuke.github.GHAppInstallation;
+import org.kohsuke.github.GHAppInstallationToken;
+import org.kohsuke.github.GitHub;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Provides an AuthorizationProvider that performs automatic token refresh.
+ */
+public class OrgAppInstallationAuthorizationProvider extends GitHub.DependentAuthorizationProvider {
+
+ private final String organizationName;
+
+ private String latestToken;
+
+ @Nonnull
+ private Instant validUntil = Instant.MIN;
+
+ /**
+ * Provides an AuthorizationProvider that performs automatic token refresh, based on an previously authenticated
+ * github client.
+ *
+ * @param organizationName
+ * The name of the organization where the application is installed
+ * @param authorizationProvider
+ * A authorization provider that returns a JWT token that can be used to refresh the App Installation
+ * token from GitHub.
+ */
+ @BetaApi
+ @Deprecated
+ public OrgAppInstallationAuthorizationProvider(String organizationName,
+ AuthorizationProvider authorizationProvider) {
+ super(authorizationProvider);
+ this.organizationName = organizationName;
+ }
+
+ @Override
+ public String getEncodedAuthorization() throws IOException {
+ synchronized (this) {
+ if (latestToken == null || Instant.now().isAfter(this.validUntil)) {
+ refreshToken();
+ }
+ return String.format("token %s", latestToken);
+ }
+ }
+
+ private void refreshToken() throws IOException {
+ GitHub gitHub = this.gitHub();
+ GHAppInstallation installationByOrganization = gitHub.getApp()
+ .getInstallationByOrganization(this.organizationName);
+ GHAppInstallationToken ghAppInstallationToken = installationByOrganization.createToken().create();
+ this.validUntil = ghAppInstallationToken.getExpiresAt().toInstant().minus(Duration.ofMinutes(5));
+ this.latestToken = Objects.requireNonNull(ghAppInstallationToken.getToken());
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/authorization/UserAuthorizationProvider.java b/src/main/java/org/kohsuke/github/authorization/UserAuthorizationProvider.java
new file mode 100644
index 0000000000..0d2c57751b
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/authorization/UserAuthorizationProvider.java
@@ -0,0 +1,22 @@
+package org.kohsuke.github.authorization;
+
+import javax.annotation.CheckForNull;
+
+/**
+ * Interface for all user-related authorization providers.
+ *
+ * {@link AuthorizationProvider}s can apply to a number of different account types. This interface applies to providers
+ * for user accounts, ones that have a login or should query the "/user" endpoint for the login matching this
+ * credential.
+ */
+public interface UserAuthorizationProvider extends AuthorizationProvider {
+
+ /**
+ * Gets the user login name.
+ *
+ * @return the user login for this provider, or {@code null} if the login value should be queried from the "/user"
+ * endpoint.
+ */
+ @CheckForNull
+ String getLogin();
+}
diff --git a/src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java b/src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java
new file mode 100644
index 0000000000..c8e8d7ddb6
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java
@@ -0,0 +1,130 @@
+package org.kohsuke.github.extras.authorization;
+
+import io.jsonwebtoken.JwtBuilder;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.kohsuke.github.authorization.AuthorizationProvider;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Date;
+
+import javax.annotation.Nonnull;
+
+/**
+ * A authorization provider that gives valid JWT tokens. These tokens are then used to create a time-based token to
+ * authenticate as an application. This token provider does not provide any kind of caching, and will always request a
+ * new token to the API.
+ */
+public class JWTTokenProvider implements AuthorizationProvider {
+
+ private final PrivateKey privateKey;
+
+ @Nonnull
+ private Instant validUntil = Instant.MIN;
+
+ private String token;
+
+ /**
+ * The identifier for the application
+ */
+ private final String applicationId;
+
+ public JWTTokenProvider(String applicationId, File keyFile) throws GeneralSecurityException, IOException {
+ this(applicationId, loadPrivateKey(keyFile.toPath()));
+ }
+
+ public JWTTokenProvider(String applicationId, Path keyPath) throws GeneralSecurityException, IOException {
+ this(applicationId, loadPrivateKey(keyPath));
+ }
+
+ public JWTTokenProvider(String applicationId, PrivateKey privateKey) {
+ this.privateKey = privateKey;
+ this.applicationId = applicationId;
+ }
+
+ @Override
+ public String getEncodedAuthorization() throws IOException {
+ synchronized (this) {
+ if (Instant.now().isAfter(validUntil)) {
+ token = refreshJWT();
+ }
+ return String.format("Bearer %s", token);
+ }
+ }
+
+ /**
+ * add dependencies for a jwt suite You can generate a key to load in this method with:
+ *
+ *
+ * openssl pkcs8 -topk8 -inform PEM -outform DER -in ~/github-api-app.private-key.pem -out ~/github-api-app.private-key.der -nocrypt
+ *
+ */
+ private static PrivateKey loadPrivateKey(Path keyPath) throws GeneralSecurityException, IOException {
+ String keyString = new String(Files.readAllBytes(keyPath), StandardCharsets.UTF_8);
+ return getPrivateKeyFromString(keyString);
+ }
+
+ /**
+ * 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 InvalidKeySpecException(
+ "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");
+ }
+
+ // Remove all comments and whitespace from PEM
+ // such as "-----BEGIN PRIVATE KEY-----" and newlines
+ String privateKeyContent = key.replaceAll("(?m)^--.*", "").replaceAll("\\s", "");
+
+ 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 InvalidKeySpecException("Failed to decode private key: " + e.getMessage(), e);
+ }
+ }
+
+ private String refreshJWT() {
+ Instant now = Instant.now();
+
+ // Token expires in 10 minutes
+ Instant expiration = Instant.now().plus(Duration.ofMinutes(10));
+
+ // Let's set the JWT Claims
+ JwtBuilder builder = Jwts.builder()
+ .setIssuedAt(Date.from(now))
+ .setExpiration(Date.from(expiration))
+ .setIssuer(this.applicationId)
+ .signWith(privateKey, SignatureAlgorithm.RS256);
+
+ // Token will refresh after 8 minutes
+ validUntil = expiration.minus(Duration.ofMinutes(2));
+
+ // Builds the JWT and serializes it to a compact, URL-safe string
+ return builder.compact();
+ }
+}
diff --git a/src/test/java/org/kohsuke/github/AbstractGHAppInstallationTest.java b/src/test/java/org/kohsuke/github/AbstractGHAppInstallationTest.java
index b25d13bc55..74576ba58c 100644
--- a/src/test/java/org/kohsuke/github/AbstractGHAppInstallationTest.java
+++ b/src/test/java/org/kohsuke/github/AbstractGHAppInstallationTest.java
@@ -2,8 +2,12 @@
import io.jsonwebtoken.Jwts;
import org.apache.commons.io.IOUtils;
+import org.kohsuke.github.authorization.AuthorizationProvider;
+import org.kohsuke.github.extras.authorization.JWTTokenProvider;
+import java.io.File;
import java.io.IOException;
+import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
@@ -21,6 +25,23 @@ public class AbstractGHAppInstallationTest extends AbstractGitHubWireMockTest {
private static String PRIVATE_KEY_FILE_APP_2 = "/ghapi-test-app-2.private-key.pem";
private static String PRIVATE_KEY_FILE_APP_3 = "/ghapi-test-app-3.private-key.pem";
+ private static AuthorizationProvider JWT_PROVIDER_1;
+ private static AuthorizationProvider JWT_PROVIDER_2;
+ private static AuthorizationProvider JWT_PROVIDER_3;
+
+ AbstractGHAppInstallationTest() {
+ try {
+ JWT_PROVIDER_1 = new JWTTokenProvider(TEST_APP_ID_1,
+ new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_1).getFile()));
+ JWT_PROVIDER_2 = new JWTTokenProvider(TEST_APP_ID_2,
+ new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_2).getFile()));
+ JWT_PROVIDER_3 = new JWTTokenProvider(TEST_APP_ID_3,
+ new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_3).getFile()));
+ } catch (GeneralSecurityException | IOException e) {
+ throw new RuntimeException("These should never fail", e);
+ }
+ }
+
private String createJwtToken(String keyFileResouceName, String appId) {
try {
String keyPEM = IOUtils.toString(this.getClass().getResource(keyFileResouceName), "US-ASCII")
@@ -63,15 +84,15 @@ private GHAppInstallation getAppInstallationWithToken(String jwtToken) throws IO
}
protected GHAppInstallation getAppInstallationWithTokenApp1() throws IOException {
- return getAppInstallationWithToken(createJwtToken(PRIVATE_KEY_FILE_APP_1, TEST_APP_ID_1));
+ return getAppInstallationWithToken(JWT_PROVIDER_1.getEncodedAuthorization());
}
protected GHAppInstallation getAppInstallationWithTokenApp2() throws IOException {
- return getAppInstallationWithToken(createJwtToken(PRIVATE_KEY_FILE_APP_2, TEST_APP_ID_2));
+ return getAppInstallationWithToken(JWT_PROVIDER_2.getEncodedAuthorization());
}
protected GHAppInstallation getAppInstallationWithTokenApp3() throws IOException {
- return getAppInstallationWithToken(createJwtToken(PRIVATE_KEY_FILE_APP_3, TEST_APP_ID_3));
+ return getAppInstallationWithToken(JWT_PROVIDER_3.getEncodedAuthorization());
}
}
diff --git a/src/test/java/org/kohsuke/github/AbstractGitHubWireMockTest.java b/src/test/java/org/kohsuke/github/AbstractGitHubWireMockTest.java
index 8cede300c7..84b838ac41 100644
--- a/src/test/java/org/kohsuke/github/AbstractGitHubWireMockTest.java
+++ b/src/test/java/org/kohsuke/github/AbstractGitHubWireMockTest.java
@@ -100,7 +100,6 @@ protected GitHubBuilder getGitHubBuilder() {
// This sets the user and password to a placeholder for wiremock testing
// This makes the tests believe they are running with permissions
// The recorded stubs will behave like they running with permissions
- builder.oauthToken = null;
builder.withPassword(STUBBED_USER_LOGIN, STUBBED_USER_PASSWORD);
}
diff --git a/src/test/java/org/kohsuke/github/GitHubConnectionTest.java b/src/test/java/org/kohsuke/github/GitHubConnectionTest.java
index 882cc3f4b7..0fb168eae8 100644
--- a/src/test/java/org/kohsuke/github/GitHubConnectionTest.java
+++ b/src/test/java/org/kohsuke/github/GitHubConnectionTest.java
@@ -1,11 +1,14 @@
package org.kohsuke.github;
import org.junit.Test;
+import org.kohsuke.github.authorization.UserAuthorizationProvider;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
+import static org.hamcrest.CoreMatchers.*;
+
/**
* Unit test for {@link GitHub}.
*/
@@ -56,19 +59,40 @@ public void testGitHubBuilderFromEnvironment() throws IOException {
Map props = new HashMap();
- props.put("login", "bogus");
- props.put("oauth", "bogus");
- props.put("password", "bogus");
- props.put("jwt", "bogus");
+ props.put("endpoint", "bogus endpoint url");
+ props.put("oauth", "bogus oauth token string");
+ setupEnvironment(props);
+ GitHubBuilder builder = GitHubBuilder.fromEnvironment();
+
+ assertThat(builder.endpoint, equalTo("bogus endpoint url"));
+
+ assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
+ assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("token bogus oauth token string"));
+ assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), nullValue());
+ props.put("login", "bogus login");
setupEnvironment(props);
+ builder = GitHubBuilder.fromEnvironment();
- GitHubBuilder builder = GitHubBuilder.fromEnvironment();
+ assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
+ assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("token bogus oauth token string"));
+ assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), equalTo("bogus login"));
+
+ props.put("jwt", "bogus jwt token string");
+ setupEnvironment(props);
+ builder = GitHubBuilder.fromEnvironment();
+
+ assertThat(builder.authorizationProvider, not(instanceOf(UserAuthorizationProvider.class)));
+ assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("Bearer bogus jwt token string"));
+
+ props.put("password", "bogus weak password");
+ setupEnvironment(props);
+ builder = GitHubBuilder.fromEnvironment();
- assertEquals("bogus", builder.user);
- assertEquals("bogus", builder.oauthToken);
- assertEquals("bogus", builder.password);
- assertEquals("bogus", builder.jwtToken);
+ assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
+ assertThat(builder.authorizationProvider.getEncodedAuthorization(),
+ equalTo("Basic Ym9ndXMgbG9naW46Ym9ndXMgd2VhayBwYXNzd29yZA=="));
+ assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), equalTo("bogus login"));
}
@@ -76,32 +100,48 @@ public void testGitHubBuilderFromEnvironment() throws IOException {
public void testGitHubBuilderFromCustomEnvironment() throws IOException {
Map props = new HashMap();
- props.put("customLogin", "bogusLogin");
- props.put("customOauth", "bogusOauth");
- props.put("customPassword", "bogusPassword");
- props.put("customEndpoint", "bogusEndpoint");
-
+ props.put("customEndpoint", "bogus endpoint url");
+ props.put("customOauth", "bogus oauth token string");
setupEnvironment(props);
-
GitHubBuilder builder = GitHubBuilder
.fromEnvironment("customLogin", "customPassword", "customOauth", "customEndpoint");
- assertEquals("bogusLogin", builder.user);
- assertEquals("bogusOauth", builder.oauthToken);
- assertEquals("bogusPassword", builder.password);
- assertEquals("bogusEndpoint", builder.endpoint);
+ assertThat(builder.endpoint, equalTo("bogus endpoint url"));
+
+ assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
+ assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("token bogus oauth token string"));
+ assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), nullValue());
+
+ props.put("customLogin", "bogus login");
+ setupEnvironment(props);
+ builder = GitHubBuilder.fromEnvironment("customLogin", "customPassword", "customOauth", "customEndpoint");
+
+ assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
+ assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("token bogus oauth token string"));
+ assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), equalTo("bogus login"));
+
+ props.put("customPassword", "bogus weak password");
+ setupEnvironment(props);
+ builder = GitHubBuilder.fromEnvironment("customLogin", "customPassword", "customOauth", "customEndpoint");
+
+ assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
+ assertThat(builder.authorizationProvider.getEncodedAuthorization(),
+ equalTo("Basic Ym9ndXMgbG9naW46Ym9ndXMgd2VhayBwYXNzd29yZA=="));
+ assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), equalTo("bogus login"));
}
@Test
public void testGithubBuilderWithAppInstallationToken() throws Exception {
- GitHubBuilder builder = new GitHubBuilder().withAppInstallationToken("bogus");
- assertEquals("bogus", builder.oauthToken);
- assertEquals("", builder.user);
+
+ GitHubBuilder builder = new GitHubBuilder().withAppInstallationToken("bogus app token");
+ assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
+ assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("token bogus app token"));
+ assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), equalTo(""));
// test authorization header is set as in the RFC6749
GitHub github = builder.build();
// change this to get a request
- assertEquals("token bogus", github.getClient().encodedAuthorization);
+ assertEquals("token bogus app token", github.getClient().getEncodedAuthorization());
assertEquals("", github.getClient().login);
}
diff --git a/src/test/java/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest.java b/src/test/java/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest.java
new file mode 100644
index 0000000000..987e9a64a2
--- /dev/null
+++ b/src/test/java/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest.java
@@ -0,0 +1,43 @@
+package org.kohsuke.github;
+
+import org.junit.Test;
+import org.kohsuke.github.authorization.ImmutableAuthorizationProvider;
+import org.kohsuke.github.authorization.OrgAppInstallationAuthorizationProvider;
+
+import java.io.IOException;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+
+public class OrgAppInstallationAuthorizationProviderTest extends AbstractGHAppInstallationTest {
+
+ public OrgAppInstallationAuthorizationProviderTest() {
+ useDefaultGitHub = false;
+ }
+
+ @Test(expected = HttpException.class)
+ public void invalidJWTTokenRaisesException() throws IOException {
+ OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider(
+ "testOrganization",
+ ImmutableAuthorizationProvider.fromJwtToken("myToken"));
+ gitHub = getGitHubBuilder().withAuthorizationProvider(provider)
+ .withEndpoint(mockGitHub.apiServer().baseUrl())
+ .build();
+
+ provider.getEncodedAuthorization();
+ }
+
+ @Test
+ public void validJWTTokenAllowsOauthTokenRequest() throws IOException {
+ OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider("hub4j-test-org",
+ ImmutableAuthorizationProvider.fromJwtToken("bogus-valid-token"));
+ gitHub = getGitHubBuilder().withAuthorizationProvider(provider)
+ .withEndpoint(mockGitHub.apiServer().baseUrl())
+ .build();
+ String encodedAuthorization = provider.getEncodedAuthorization();
+
+ assertThat(encodedAuthorization, notNullValue());
+ assertThat(encodedAuthorization, equalTo("token v1.9a12d913f980a45a16ac9c3a9d34d9b7sa314cb6"));
+ }
+
+}
diff --git a/src/test/java/org/kohsuke/github/extras/authorization/JWTTokenProviderTest.java b/src/test/java/org/kohsuke/github/extras/authorization/JWTTokenProviderTest.java
new file mode 100644
index 0000000000..8c55558f96
--- /dev/null
+++ b/src/test/java/org/kohsuke/github/extras/authorization/JWTTokenProviderTest.java
@@ -0,0 +1,47 @@
+package org.kohsuke.github.extras.authorization;
+
+import org.junit.Test;
+import org.kohsuke.github.AbstractGitHubWireMockTest;
+import org.kohsuke.github.GitHub;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+/*
+ * This test will request an application ensuring that the header for the "Authorization" matches a valid JWT token.
+ * A JWT token in the Authorization header will always start with "ey" which is always the start of the base64
+ * encoding of the JWT Header , so a valid header will look like this:
+ *
+ *
+ * Authorization: Bearer ey{rest of the header}.{payload}.{signature}
+ *
+ *
+ * Matched by the regular expression:
+ *
+ *
+ * ^Bearer (?ey\S*)\.(?\S*)\.(?\S*)$
+ *
+ *
+ * Which is present in the wiremock matcher. Note that we need to use a matcher because the JWT token is encoded
+ * with a private key and a random nonce, so it will never be the same (under normal conditions). For more
+ * information on the format of a JWT token, see: https://jwt.io/introduction/
+ */
+public class JWTTokenProviderTest extends AbstractGitHubWireMockTest {
+
+ private static String TEST_APP_ID_2 = "83009";
+ private static String PRIVATE_KEY_FILE_APP_2 = "/ghapi-test-app-2.private-key.pem";
+
+ @Test
+ public void testAuthorizationHeaderPattern() throws GeneralSecurityException, IOException {
+ JWTTokenProvider jwtTokenProvider = new JWTTokenProvider(TEST_APP_ID_2,
+ new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_2).getFile()));
+ GitHub gh = getGitHubBuilder().withEndpoint(mockGitHub.apiServer().baseUrl())
+ .withAuthorizationProvider(jwtTokenProvider)
+ .build();
+
+ // Request the application, the wiremock matcher will ensure that the header
+ // for the authorization is present and has a the format of a valid JWT token
+ gh.getApp();
+ }
+
+}
diff --git a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/invalidJWTTokenRaisesException/mappings/app-2.json b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/invalidJWTTokenRaisesException/mappings/app-2.json
new file mode 100644
index 0000000000..5849fd75c7
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/invalidJWTTokenRaisesException/mappings/app-2.json
@@ -0,0 +1,35 @@
+{
+ "id": "960b4085-803f-43aa-a291-ccb6fd003adb",
+ "name": "app",
+ "request": {
+ "url": "/app",
+ "method": "GET",
+ "headers": {
+ "Accept": {
+ "equalTo": "application/vnd.github.machine-man-preview+json"
+ }
+ }
+ },
+ "response": {
+ "status": 401,
+ "body": "{\"message\":\"A JSON web token could not be decoded\",\"documentation_url\":\"https://docs.github.com/rest\"}",
+ "headers": {
+ "Date": "Tue, 29 Sep 2020 12:35:35 GMT",
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "GitHub.com",
+ "Status": "401 Unauthorized",
+ "X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
+ "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+ "X-Frame-Options": "deny",
+ "X-Content-Type-Options": "nosniff",
+ "X-XSS-Protection": "1; mode=block",
+ "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+ "Content-Security-Policy": "default-src 'none'",
+ "Vary": "Accept-Encoding, Accept, X-Requested-With",
+ "X-GitHub-Request-Id": "D236:47C4:1909E17E:1DD010FD:5F732A16"
+ }
+ },
+ "uuid": "960b4085-803f-43aa-a291-ccb6fd003adb",
+ "persistent": true,
+ "insertionIndex": 2
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/invalidJWTTokenRaisesException/mappings/user-1.json b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/invalidJWTTokenRaisesException/mappings/user-1.json
new file mode 100644
index 0000000000..feea2b1f2d
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/invalidJWTTokenRaisesException/mappings/user-1.json
@@ -0,0 +1,39 @@
+{
+ "id": "31df960e-9966-4b89-8a99-0d6688accca9",
+ "name": "user",
+ "request": {
+ "url": "/user",
+ "method": "GET",
+ "headers": {
+ "Accept": {
+ "equalTo": "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"
+ }
+ }
+ },
+ "response": {
+ "status": 401,
+ "body": "{\"message\":\"Bad credentials\",\"documentation_url\":\"https://docs.github.com/rest\"}",
+ "headers": {
+ "Date": "Tue, 29 Sep 2020 12:35:34 GMT",
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "GitHub.com",
+ "Status": "401 Unauthorized",
+ "X-GitHub-Media-Type": "unknown, github.v3",
+ "X-RateLimit-Limit": "60",
+ "X-RateLimit-Remaining": "55",
+ "X-RateLimit-Reset": "1601386475",
+ "X-RateLimit-Used": "5",
+ "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+ "X-Frame-Options": "deny",
+ "X-Content-Type-Options": "nosniff",
+ "X-XSS-Protection": "1; mode=block",
+ "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+ "Content-Security-Policy": "default-src 'none'",
+ "Vary": "Accept-Encoding, Accept, X-Requested-With",
+ "X-GitHub-Request-Id": "D236:47C4:1909E038:1DD010AD:5F732A16"
+ }
+ },
+ "uuid": "31df960e-9966-4b89-8a99-0d6688accca9",
+ "persistent": true,
+ "insertionIndex": 1
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/__files/app-2.json b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/__files/app-2.json
new file mode 100644
index 0000000000..4442d5c635
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/__files/app-2.json
@@ -0,0 +1,39 @@
+{
+ "id": 79253,
+ "slug": "hub4j-test-application",
+ "node_id": "MDM6QXBwNzkyNTM=",
+ "owner": {
+ "login": "hub4j-test-org",
+ "id": 70590530,
+ "node_id": "MDEyOk9yZ2FuaXphdGlvbjcwNTkwNTMw",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/70590530?v=4",
+ "gravatar_id": "",
+ "url": "https://api.github.com/users/hub4j-test-org",
+ "html_url": "https://github.com/hub4j-test-org",
+ "followers_url": "https://api.github.com/users/hub4j-test-org/followers",
+ "following_url": "https://api.github.com/users/hub4j-test-org/following{/other_user}",
+ "gists_url": "https://api.github.com/users/hub4j-test-org/gists{/gist_id}",
+ "starred_url": "https://api.github.com/users/hub4j-test-org/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/hub4j-test-org/subscriptions",
+ "organizations_url": "https://api.github.com/users/hub4j-test-org/orgs",
+ "repos_url": "https://api.github.com/users/hub4j-test-org/repos",
+ "events_url": "https://api.github.com/users/hub4j-test-org/events{/privacy}",
+ "received_events_url": "https://api.github.com/users/hub4j-test-org/received_events",
+ "type": "Organization",
+ "site_admin": false
+ },
+ "name": "hub4j-test-application",
+ "description": "",
+ "external_url": "https://example.com",
+ "html_url": "https://github.com/apps/hub4j-test-application",
+ "created_at": "2020-09-01T14:56:16Z",
+ "updated_at": "2020-09-01T14:56:16Z",
+ "permissions": {
+ "metadata": "read",
+ "pull_requests": "write"
+ },
+ "events": [
+ "pull_request"
+ ],
+ "installations_count": 1
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/__files/orgs_hub4j-test-org_installation-3.json b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/__files/orgs_hub4j-test-org_installation-3.json
new file mode 100644
index 0000000000..80a6c2db36
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/__files/orgs_hub4j-test-org_installation-3.json
@@ -0,0 +1,43 @@
+{
+ "id": 11575015,
+ "account": {
+ "login": "hub4j-test-org",
+ "id": 70590530,
+ "node_id": "MDEyOk9yZ2FuaXphdGlvbjcwNTkwNTMw",
+ "avatar_url": "https://avatars1.githubusercontent.com/u/70590530?v=4",
+ "gravatar_id": "",
+ "url": "https://api.github.com/users/hub4j-test-org",
+ "html_url": "https://github.com/hub4j-test-org",
+ "followers_url": "https://api.github.com/users/hub4j-test-org/followers",
+ "following_url": "https://api.github.com/users/hub4j-test-org/following{/other_user}",
+ "gists_url": "https://api.github.com/users/hub4j-test-org/gists{/gist_id}",
+ "starred_url": "https://api.github.com/users/hub4j-test-org/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/hub4j-test-org/subscriptions",
+ "organizations_url": "https://api.github.com/users/hub4j-test-org/orgs",
+ "repos_url": "https://api.github.com/users/hub4j-test-org/repos",
+ "events_url": "https://api.github.com/users/hub4j-test-org/events{/privacy}",
+ "received_events_url": "https://api.github.com/users/hub4j-test-org/received_events",
+ "type": "Organization",
+ "site_admin": false
+ },
+ "repository_selection": "all",
+ "access_tokens_url": "https://api.github.com/app/installations/11575015/access_tokens",
+ "repositories_url": "https://api.github.com/installation/repositories",
+ "html_url": "https://github.com/organizations/hub4j-test-org/settings/installations/11575015",
+ "app_id": 79253,
+ "app_slug": "hub4j-test-application",
+ "target_id": 70590530,
+ "target_type": "Organization",
+ "permissions": {
+ "metadata": "read",
+ "pull_requests": "write"
+ },
+ "events": [
+ "pull_request"
+ ],
+ "created_at": "2020-09-01T14:56:49.000Z",
+ "updated_at": "2020-09-01T14:56:49.000Z",
+ "single_file_name": null,
+ "suspended_by": null,
+ "suspended_at": null
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/app-2.json b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/app-2.json
new file mode 100644
index 0000000000..86b4f0076e
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/app-2.json
@@ -0,0 +1,41 @@
+{
+ "id": "7b483ea8-ace3-4af3-ae23-b081d717fa53",
+ "name": "app",
+ "request": {
+ "url": "/app",
+ "method": "GET",
+ "headers": {
+ "Accept": {
+ "equalTo": "application/vnd.github.machine-man-preview+json"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "bodyFileName": "app-2.json",
+ "headers": {
+ "Date": "Tue, 29 Sep 2020 12:35:36 GMT",
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "GitHub.com",
+ "Status": "200 OK",
+ "Cache-Control": "public, max-age=60, s-maxage=60",
+ "Vary": [
+ "Accept",
+ "Accept-Encoding, Accept, X-Requested-With",
+ "Accept-Encoding"
+ ],
+ "ETag": "W/\"a4f1cab410e5b80ee9775d1ecb4d3296f067ddcdfa22ba2122dd382c992b55fe\"",
+ "X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
+ "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+ "X-Frame-Options": "deny",
+ "X-Content-Type-Options": "nosniff",
+ "X-XSS-Protection": "1; mode=block",
+ "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+ "Content-Security-Policy": "default-src 'none'",
+ "X-GitHub-Request-Id": "D11A:F68D:17924B62:1C1232BE:5F732A18"
+ }
+ },
+ "uuid": "7b483ea8-ace3-4af3-ae23-b081d717fa53",
+ "persistent": true,
+ "insertionIndex": 2
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/app_installations_11575015_access_tokens-4.json b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/app_installations_11575015_access_tokens-4.json
new file mode 100644
index 0000000000..68600667e9
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/app_installations_11575015_access_tokens-4.json
@@ -0,0 +1,48 @@
+{
+ "id": "7e25da60-68c9-41c5-b603-359192783583",
+ "name": "app_installations_11575015_access_tokens",
+ "request": {
+ "url": "/app/installations/11575015/access_tokens",
+ "method": "POST",
+ "headers": {
+ "Accept": {
+ "equalTo": "application/vnd.github.machine-man-preview+json"
+ }
+ },
+ "bodyPatterns": [
+ {
+ "equalToJson": "{}",
+ "ignoreArrayOrder": true,
+ "ignoreExtraElements": false
+ }
+ ]
+ },
+ "response": {
+ "status": 201,
+ "body": "{\"token\":\"v1.9a12d913f980a45a16ac9c3a9d34d9b7sa314cb6\",\"expires_at\":\"2020-09-29T13:35:37Z\",\"permissions\":{\"metadata\":\"read\",\"pull_requests\":\"write\"},\"repository_selection\":\"all\"}",
+ "headers": {
+ "Date": "Tue, 29 Sep 2020 12:35:37 GMT",
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "GitHub.com",
+ "Status": "201 Created",
+ "Cache-Control": "public, max-age=60, s-maxage=60",
+ "Vary": [
+ "Accept",
+ "Accept-Encoding, Accept, X-Requested-With",
+ "Accept-Encoding"
+ ],
+ "ETag": "\"168d81847da026cae71dddc5658dc87c05a2b6945d4e635787c451df823fc72a\"",
+ "X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
+ "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+ "X-Frame-Options": "deny",
+ "X-Content-Type-Options": "nosniff",
+ "X-XSS-Protection": "1; mode=block",
+ "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+ "Content-Security-Policy": "default-src 'none'",
+ "X-GitHub-Request-Id": "D11A:F68D:17924C69:1C12341C:5F732A18"
+ }
+ },
+ "uuid": "7e25da60-68c9-41c5-b603-359192783583",
+ "persistent": true,
+ "insertionIndex": 4
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/orgs_hub4j-test-org_installation-3.json b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/orgs_hub4j-test-org_installation-3.json
new file mode 100644
index 0000000000..54e1ea9d4d
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/orgs_hub4j-test-org_installation-3.json
@@ -0,0 +1,41 @@
+{
+ "id": "9ffe1e34-1d0e-495a-abdc-86fdf1d15334",
+ "name": "orgs_hub4j-test-org_installation",
+ "request": {
+ "url": "/orgs/hub4j-test-org/installation",
+ "method": "GET",
+ "headers": {
+ "Accept": {
+ "equalTo": "application/vnd.github.machine-man-preview+json"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "bodyFileName": "orgs_hub4j-test-org_installation-3.json",
+ "headers": {
+ "Date": "Tue, 29 Sep 2020 12:35:36 GMT",
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "GitHub.com",
+ "Status": "200 OK",
+ "Cache-Control": "public, max-age=60, s-maxage=60",
+ "Vary": [
+ "Accept",
+ "Accept-Encoding, Accept, X-Requested-With",
+ "Accept-Encoding"
+ ],
+ "ETag": "W/\"5fa17d9ba74cf1c58441056ab43311b39f39e78976e8524ad3962278c5224955\"",
+ "X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
+ "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+ "X-Frame-Options": "deny",
+ "X-Content-Type-Options": "nosniff",
+ "X-XSS-Protection": "1; mode=block",
+ "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+ "Content-Security-Policy": "default-src 'none'",
+ "X-GitHub-Request-Id": "D11A:F68D:17924BFB:1C12335A:5F732A18"
+ }
+ },
+ "uuid": "9ffe1e34-1d0e-495a-abdc-86fdf1d15334",
+ "persistent": true,
+ "insertionIndex": 3
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/user-1.json b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/user-1.json
new file mode 100644
index 0000000000..ca0c3dee38
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/OrgAppInstallationAuthorizationProviderTest/wiremock/validJWTTokenAllowsOauthTokenRequest/mappings/user-1.json
@@ -0,0 +1,39 @@
+{
+ "id": "85ae1237-62c3-4f75-888b-8d751677aa07",
+ "name": "user",
+ "request": {
+ "url": "/user",
+ "method": "GET",
+ "headers": {
+ "Accept": {
+ "equalTo": "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"
+ }
+ }
+ },
+ "response": {
+ "status": 401,
+ "body": "{\"message\":\"Bad credentials\",\"documentation_url\":\"https://docs.github.com/rest\"}",
+ "headers": {
+ "Date": "Tue, 29 Sep 2020 12:35:36 GMT",
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "GitHub.com",
+ "Status": "401 Unauthorized",
+ "X-GitHub-Media-Type": "unknown, github.v3",
+ "X-RateLimit-Limit": "60",
+ "X-RateLimit-Remaining": "53",
+ "X-RateLimit-Reset": "1601386475",
+ "X-RateLimit-Used": "7",
+ "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+ "X-Frame-Options": "deny",
+ "X-Content-Type-Options": "nosniff",
+ "X-XSS-Protection": "1; mode=block",
+ "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+ "Content-Security-Policy": "default-src 'none'",
+ "Vary": "Accept-Encoding, Accept, X-Requested-With",
+ "X-GitHub-Request-Id": "D11A:F68D:17924B00:1C12327C:5F732A17"
+ }
+ },
+ "uuid": "85ae1237-62c3-4f75-888b-8d751677aa07",
+ "persistent": true,
+ "insertionIndex": 1
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/extras/authorization/JWTTokenProviderTest/wiremock/testAuthorizationHeaderPattern/__files/app-1.json b/src/test/resources/org/kohsuke/github/extras/authorization/JWTTokenProviderTest/wiremock/testAuthorizationHeaderPattern/__files/app-1.json
new file mode 100644
index 0000000000..8cc884b88a
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/extras/authorization/JWTTokenProviderTest/wiremock/testAuthorizationHeaderPattern/__files/app-1.json
@@ -0,0 +1,34 @@
+{
+ "id": 83009,
+ "slug": "ghapi-test-app-2",
+ "node_id": "MDM6QXBwODMwMDk=",
+ "owner": {
+ "login": "hub4j-test-org",
+ "id": 7544739,
+ "node_id": "MDEyOk9yZ2FuaXphdGlvbjc1NDQ3Mzk=",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/7544739?v=4",
+ "gravatar_id": "",
+ "url": "https://api.github.com/users/hub4j-test-org",
+ "html_url": "https://github.com/hub4j-test-org",
+ "followers_url": "https://api.github.com/users/hub4j-test-org/followers",
+ "following_url": "https://api.github.com/users/hub4j-test-org/following{/other_user}",
+ "gists_url": "https://api.github.com/users/hub4j-test-org/gists{/gist_id}",
+ "starred_url": "https://api.github.com/users/hub4j-test-org/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/hub4j-test-org/subscriptions",
+ "organizations_url": "https://api.github.com/users/hub4j-test-org/orgs",
+ "repos_url": "https://api.github.com/users/hub4j-test-org/repos",
+ "events_url": "https://api.github.com/users/hub4j-test-org/events{/privacy}",
+ "received_events_url": "https://api.github.com/users/hub4j-test-org/received_events",
+ "type": "Organization",
+ "site_admin": false
+ },
+ "name": "GHApi Test app 2",
+ "description": "",
+ "external_url": "https://localhost",
+ "html_url": "https://github.com/apps/ghapi-test-app-2",
+ "created_at": "2020-09-30T15:02:20Z",
+ "updated_at": "2020-09-30T15:02:20Z",
+ "permissions": {},
+ "events": [],
+ "installations_count": 1
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/extras/authorization/JWTTokenProviderTest/wiremock/testAuthorizationHeaderPattern/mappings/app-1.json b/src/test/resources/org/kohsuke/github/extras/authorization/JWTTokenProviderTest/wiremock/testAuthorizationHeaderPattern/mappings/app-1.json
new file mode 100644
index 0000000000..f74c924f95
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/extras/authorization/JWTTokenProviderTest/wiremock/testAuthorizationHeaderPattern/mappings/app-1.json
@@ -0,0 +1,44 @@
+{
+ "id": "bb7cf5bb-45b3-fba2-afd8-939b2c24787a",
+ "name": "app",
+ "request": {
+ "url": "/app",
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "^Bearer (?ey\\S*)\\.(?\\S*)\\.(?\\S*)$"
+ },
+ "Accept": {
+ "equalTo": "application/vnd.github.machine-man-preview+json"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "bodyFileName": "app-1.json",
+ "headers": {
+ "Date": "Thu, 05 Nov 2020 20:42:31 GMT",
+ "Content-Type": "application/json; charset=utf-8",
+ "Server": "GitHub.com",
+ "Status": "200 OK",
+ "Cache-Control": "public, max-age=60, s-maxage=60",
+ "Vary": [
+ "Accept",
+ "Accept-Encoding, Accept, X-Requested-With",
+ "Accept-Encoding"
+ ],
+ "ETag": "W/\"b3d319dbb4dba93fbda071208d874e5ab566d827e1ad1d7dc59f26d68694dc48\"",
+ "X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
+ "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+ "X-Frame-Options": "deny",
+ "X-Content-Type-Options": "nosniff",
+ "X-XSS-Protection": "1; mode=block",
+ "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+ "Content-Security-Policy": "default-src 'none'",
+ "X-GitHub-Request-Id": "9294:AE05:BDAC761:DB35838:5FA463B6"
+ }
+ },
+ "uuid": "bb7cf5bb-45b3-fba2-afd8-939b2c24787a",
+ "persistent": true,
+ "insertionIndex": 1
+}
\ No newline at end of file