Merge pull request #945 from MarcosCela/feat/credential-provider-refresh

Feat/credential provider refresh
This commit is contained in:
Liam Newman
2021-01-14 10:37:30 -08:00
committed by GitHub
26 changed files with 1158 additions and 138 deletions

13
pom.xml
View File

@@ -45,6 +45,7 @@
<jacoco.coverage.target.class.method>0.25</jacoco.coverage.target.class.method> <jacoco.coverage.target.class.method>0.25</jacoco.coverage.target.class.method>
<!-- For non-ci builds we'd like the build to still complete if jacoco metrics aren't met. --> <!-- For non-ci builds we'd like the build to still complete if jacoco metrics aren't met. -->
<jacoco.haltOnFailure>false</jacoco.haltOnFailure> <jacoco.haltOnFailure>false</jacoco.haltOnFailure>
<jjwt.suite.version>0.11.2</jjwt.suite.version>
</properties> </properties>
<build> <build>
@@ -489,20 +490,20 @@
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId> <artifactId>jjwt-api</artifactId>
<version>0.11.2</version> <version>${jjwt.suite.version}</version>
<scope>test</scope> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId> <artifactId>jjwt-impl</artifactId>
<version>0.11.2</version> <version>${jjwt.suite.version}</version>
<scope>test</scope> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version> <version>${jjwt.suite.version}</version>
<scope>test</scope> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.squareup.okio</groupId> <groupId>com.squareup.okio</groupId>

View File

@@ -12,6 +12,7 @@ import java.util.Locale;
public enum GHEvent { public enum GHEvent {
CHECK_RUN, CHECK_RUN,
CHECK_SUITE, CHECK_SUITE,
CODE_SCANNING_ALERT,
COMMIT_COMMENT, COMMIT_COMMENT,
CONTENT_REFERENCE, CONTENT_REFERENCE,
CREATE, CREATE,

View File

@@ -26,6 +26,7 @@ package org.kohsuke.github;
import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.ObjectWriter;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import org.kohsuke.github.authorization.AuthorizationProvider;
import org.kohsuke.github.internal.Previews; import org.kohsuke.github.internal.Previews;
import java.io.*; import java.io.*;
@@ -94,39 +95,112 @@ public class GitHub {
* "http://ghe.acme.com/api/v3". Note that GitHub Enterprise has <code>/api/v3</code> in the URL. For * "http://ghe.acme.com/api/v3". Note that GitHub Enterprise has <code>/api/v3</code> in the URL. For
* historical reasons, this parameter still accepts the bare domain name, but that's considered * historical reasons, this parameter still accepts the bare domain name, but that's considered
* deprecated. Password is also considered deprecated as it is no longer required for api usage. * deprecated. Password is also considered deprecated as it is no longer required for api usage.
* @param login
* The user ID on GitHub that you are logging in as. Can be omitted if the OAuth token is provided or if
* logging in anonymously. Specifying this would save one API call.
* @param oauthAccessToken
* Secret OAuth token.
* @param password
* User's password. Always used in conjunction with the {@code login} parameter
* @param connector * @param connector
* HttpConnector to use. Pass null to use default connector. * a connector
* @param rateLimitHandler
* rateLimitHandler
* @param abuseLimitHandler
* abuseLimitHandler
* @param rateLimitChecker
* rateLimitChecker
* @param authorizationProvider
* a authorization provider
*/ */
GitHub(String apiUrl, GitHub(String apiUrl,
String login,
String oauthAccessToken,
String jwtToken,
String password,
HttpConnector connector, HttpConnector connector,
RateLimitHandler rateLimitHandler, RateLimitHandler rateLimitHandler,
AbuseLimitHandler abuseLimitHandler, AbuseLimitHandler abuseLimitHandler,
GitHubRateLimitChecker rateLimitChecker) throws IOException { GitHubRateLimitChecker rateLimitChecker,
AuthorizationProvider authorizationProvider) throws IOException {
if (authorizationProvider instanceof DependentAuthorizationProvider) {
((DependentAuthorizationProvider) authorizationProvider).bind(this);
}
this.client = new GitHubHttpUrlConnectionClient(apiUrl, this.client = new GitHubHttpUrlConnectionClient(apiUrl,
login,
oauthAccessToken,
jwtToken,
password,
connector, connector,
rateLimitHandler, rateLimitHandler,
abuseLimitHandler, abuseLimitHandler,
rateLimitChecker, rateLimitChecker,
(myself) -> setMyself(myself)); (myself) -> setMyself(myself),
authorizationProvider);
users = new ConcurrentHashMap<>(); users = new ConcurrentHashMap<>();
orgs = new ConcurrentHashMap<>(); orgs = new ConcurrentHashMap<>();
} }
private GitHub(GitHubClient client) {
this.client = client;
users = new ConcurrentHashMap<>();
orgs = new ConcurrentHashMap<>();
}
public static abstract class DependentAuthorizationProvider implements AuthorizationProvider {
private GitHub baseGitHub;
private GitHub gitHub;
private final AuthorizationProvider authorizationProvider;
/**
* An AuthorizationProvider that requires an authenticated GitHub instance to provide its authorization.
*
* @param authorizationProvider
* A authorization provider to be used when refreshing this authorization provider.
*/
@BetaApi
@Deprecated
protected DependentAuthorizationProvider(AuthorizationProvider authorizationProvider) {
this.authorizationProvider = authorizationProvider;
}
/**
* Binds this authorization provider to a github instance.
*
* Only needs to be implemented by dynamic credentials providers that use a github instance in order to refresh.
*
* @param github
* The github instance to be used for refreshing dynamic credentials
*/
synchronized void bind(GitHub github) {
if (baseGitHub != null) {
throw new IllegalStateException("Already bound to another GitHub instance.");
}
this.baseGitHub = github;
}
protected synchronized final GitHub gitHub() {
if (gitHub == null) {
gitHub = new GitHub.AuthorizationRefreshGitHubWrapper(this.baseGitHub, authorizationProvider);
}
return gitHub;
}
}
private static class AuthorizationRefreshGitHubWrapper extends GitHub {
private final AuthorizationProvider authorizationProvider;
AuthorizationRefreshGitHubWrapper(GitHub github, AuthorizationProvider authorizationProvider) {
super(github.client);
this.authorizationProvider = authorizationProvider;
// no dependent authorization providers nest like this currently, but they might in future
if (authorizationProvider instanceof DependentAuthorizationProvider) {
((DependentAuthorizationProvider) authorizationProvider).bind(this);
}
}
@Nonnull
@Override
Requester createRequest() {
try {
// Override
return super.createRequest().setHeader("Authorization", authorizationProvider.getEncodedAuthorization())
.rateLimit(RateLimitTarget.NONE);
} catch (IOException e) {
throw new GHException("Failed to create requester to refresh credentials", e);
}
}
}
/** /**
* Obtains the credential from "~/.github" or from the System Environment Properties. * Obtains the credential from "~/.github" or from the System Environment Properties.
* *

View File

@@ -1,6 +1,8 @@
package org.kohsuke.github; package org.kohsuke.github;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.kohsuke.github.authorization.AuthorizationProvider;
import org.kohsuke.github.authorization.ImmutableAuthorizationProvider;
import org.kohsuke.github.extras.ImpatientHttpConnector; import org.kohsuke.github.extras.ImpatientHttpConnector;
import java.io.File; import java.io.File;
@@ -24,16 +26,13 @@ public class GitHubBuilder implements Cloneable {
// default scoped so unit tests can read them. // default scoped so unit tests can read them.
/* private */ String endpoint = GitHubClient.GITHUB_URL; /* private */ String endpoint = GitHubClient.GITHUB_URL;
/* private */ String user;
/* private */ String password;
/* private */ String oauthToken;
/* private */ String jwtToken;
private HttpConnector connector; private HttpConnector connector;
private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT; private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT;
private AbuseLimitHandler abuseLimitHandler = AbuseLimitHandler.WAIT; private AbuseLimitHandler abuseLimitHandler = AbuseLimitHandler.WAIT;
private GitHubRateLimitChecker rateLimitChecker = new GitHubRateLimitChecker(); private GitHubRateLimitChecker rateLimitChecker = new GitHubRateLimitChecker();
/* private */ AuthorizationProvider authorizationProvider = AuthorizationProvider.ANONYMOUS;
/** /**
* Instantiates a new Git hub builder. * Instantiates a new Git hub builder.
@@ -61,13 +60,13 @@ public class GitHubBuilder implements Cloneable {
builder = fromEnvironment(); builder = fromEnvironment();
if (builder.oauthToken != null || builder.user != null || builder.jwtToken != null) if (builder.authorizationProvider != null)
return builder; return builder;
try { try {
builder = fromPropertyFile(); builder = fromPropertyFile();
if (builder.oauthToken != null || builder.user != null || builder.jwtToken != null) if (builder.authorizationProvider != null)
return builder; return builder;
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
// fall through // fall through
@@ -215,9 +214,20 @@ public class GitHubBuilder implements Cloneable {
*/ */
public static GitHubBuilder fromProperties(Properties props) { public static GitHubBuilder fromProperties(Properties props) {
GitHubBuilder self = new GitHubBuilder(); GitHubBuilder self = new GitHubBuilder();
self.withOAuthToken(props.getProperty("oauth"), props.getProperty("login")); String oauth = props.getProperty("oauth");
self.withJwtToken(props.getProperty("jwt")); String jwt = props.getProperty("jwt");
self.withPassword(props.getProperty("login"), props.getProperty("password")); String login = props.getProperty("login");
String password = props.getProperty("password");
if (oauth != null) {
self.withOAuthToken(oauth, login);
}
if (jwt != null) {
self.withJwtToken(jwt);
}
if (password != null) {
self.withPassword(login, password);
}
self.withEndpoint(props.getProperty("endpoint", GitHubClient.GITHUB_URL)); self.withEndpoint(props.getProperty("endpoint", GitHubClient.GITHUB_URL));
return self; return self;
} }
@@ -247,9 +257,7 @@ public class GitHubBuilder implements Cloneable {
* @return the git hub builder * @return the git hub builder
*/ */
public GitHubBuilder withPassword(String user, String password) { public GitHubBuilder withPassword(String user, String password) {
this.user = user; return withAuthorizationProvider(ImmutableAuthorizationProvider.fromLoginAndPassword(user, password));
this.password = password;
return this;
} }
/** /**
@@ -260,7 +268,7 @@ public class GitHubBuilder implements Cloneable {
* @return the git hub builder * @return the git hub builder
*/ */
public GitHubBuilder withOAuthToken(String oauthToken) { public GitHubBuilder withOAuthToken(String oauthToken) {
return withOAuthToken(oauthToken, null); return withAuthorizationProvider(ImmutableAuthorizationProvider.fromOauthToken(oauthToken));
} }
/** /**
@@ -273,8 +281,21 @@ public class GitHubBuilder implements Cloneable {
* @return the git hub builder * @return the git hub builder
*/ */
public GitHubBuilder withOAuthToken(String oauthToken, String user) { public GitHubBuilder withOAuthToken(String oauthToken, String user) {
this.oauthToken = oauthToken; return withAuthorizationProvider(ImmutableAuthorizationProvider.fromOauthToken(oauthToken, user));
this.user = user; }
/**
* Configures a {@link AuthorizationProvider} for this builder
*
* There can be only one authorization provider per client instance.
*
* @param authorizationProvider
* the authorization provider
* @return the git hub builder
*
*/
public GitHubBuilder withAuthorizationProvider(final AuthorizationProvider authorizationProvider) {
this.authorizationProvider = authorizationProvider;
return this; return this;
} }
@@ -287,7 +308,7 @@ public class GitHubBuilder implements Cloneable {
* @see GHAppInstallation#createToken(java.util.Map) GHAppInstallation#createToken(java.util.Map) * @see GHAppInstallation#createToken(java.util.Map) GHAppInstallation#createToken(java.util.Map)
*/ */
public GitHubBuilder withAppInstallationToken(String appInstallationToken) { public GitHubBuilder withAppInstallationToken(String appInstallationToken) {
return withOAuthToken(appInstallationToken, ""); return withAuthorizationProvider(ImmutableAuthorizationProvider.fromAppInstallationToken(appInstallationToken));
} }
/** /**
@@ -298,8 +319,7 @@ public class GitHubBuilder implements Cloneable {
* @return the git hub builder * @return the git hub builder
*/ */
public GitHubBuilder withJwtToken(String jwtToken) { public GitHubBuilder withJwtToken(String jwtToken) {
this.jwtToken = jwtToken; return withAuthorizationProvider(ImmutableAuthorizationProvider.fromJwtToken(jwtToken));
return this;
} }
/** /**
@@ -421,14 +441,11 @@ public class GitHubBuilder implements Cloneable {
*/ */
public GitHub build() throws IOException { public GitHub build() throws IOException {
return new GitHub(endpoint, return new GitHub(endpoint,
user,
oauthToken,
jwtToken,
password,
connector, connector,
rateLimitHandler, rateLimitHandler,
abuseLimitHandler, abuseLimitHandler,
rateLimitChecker); rateLimitChecker,
authorizationProvider);
} }
@Override @Override

View File

@@ -1,33 +1,19 @@
package org.kohsuke.github; package org.kohsuke.github;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.introspect.VisibilityChecker; import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.kohsuke.github.authorization.AuthorizationProvider;
import org.kohsuke.github.authorization.UserAuthorizationProvider;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
import java.net.HttpURLConnection; import java.net.*;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Base64; import java.util.*;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -56,17 +42,13 @@ abstract class GitHubClient {
static final int retryTimeoutMillis = 100; static final int retryTimeoutMillis = 100;
/* private */ final String login; /* private */ final String login;
/**
* Value of the authorization header to be sent with the request.
*/
/* private */ final String encodedAuthorization;
// Cache of myself object. // Cache of myself object.
private final String apiUrl; private final String apiUrl;
protected final RateLimitHandler rateLimitHandler; protected final RateLimitHandler rateLimitHandler;
protected final AbuseLimitHandler abuseLimitHandler; protected final AbuseLimitHandler abuseLimitHandler;
private final GitHubRateLimitChecker rateLimitChecker; private final GitHubRateLimitChecker rateLimitChecker;
private final AuthorizationProvider authorizationProvider;
private HttpConnector connector; private HttpConnector connector;
@@ -91,15 +73,12 @@ abstract class GitHubClient {
} }
GitHubClient(String apiUrl, GitHubClient(String apiUrl,
String login,
String oauthAccessToken,
String jwtToken,
String password,
HttpConnector connector, HttpConnector connector,
RateLimitHandler rateLimitHandler, RateLimitHandler rateLimitHandler,
AbuseLimitHandler abuseLimitHandler, AbuseLimitHandler abuseLimitHandler,
GitHubRateLimitChecker rateLimitChecker, GitHubRateLimitChecker rateLimitChecker,
Consumer<GHMyself> myselfConsumer) throws IOException { Consumer<GHMyself> myselfConsumer,
AuthorizationProvider authorizationProvider) throws IOException {
if (apiUrl.endsWith("/")) { if (apiUrl.endsWith("/")) {
apiUrl = apiUrl.substring(0, apiUrl.length() - 1); // normalize apiUrl = apiUrl.substring(0, apiUrl.length() - 1); // normalize
@@ -111,33 +90,38 @@ abstract class GitHubClient {
this.apiUrl = apiUrl; this.apiUrl = apiUrl;
this.connector = connector; this.connector = connector;
if (oauthAccessToken != null) { // Prefer credential configuration via provider
encodedAuthorization = "token " + oauthAccessToken; this.authorizationProvider = authorizationProvider;
} else {
if (jwtToken != null) {
encodedAuthorization = "Bearer " + jwtToken;
} else if (password != null) {
String authorization = (login + ':' + password);
String charsetName = StandardCharsets.UTF_8.name();
encodedAuthorization = "Basic "
+ Base64.getEncoder().encodeToString(authorization.getBytes(charsetName));
} else {// anonymous access
encodedAuthorization = null;
}
}
this.rateLimitHandler = rateLimitHandler; this.rateLimitHandler = rateLimitHandler;
this.abuseLimitHandler = abuseLimitHandler; this.abuseLimitHandler = abuseLimitHandler;
this.rateLimitChecker = rateLimitChecker; this.rateLimitChecker = rateLimitChecker;
if (login == null && encodedAuthorization != null && jwtToken == null) { this.login = getCurrentUser(myselfConsumer);
GHMyself myself = fetch(GHMyself.class, "/user"); }
login = myself.getLogin();
if (myselfConsumer != null) { private String getCurrentUser(Consumer<GHMyself> myselfConsumer) throws IOException {
myselfConsumer.accept(myself); String login = null;
if (this.authorizationProvider instanceof UserAuthorizationProvider
&& this.authorizationProvider.getEncodedAuthorization() != null) {
UserAuthorizationProvider userAuthorizationProvider = (UserAuthorizationProvider) this.authorizationProvider;
login = userAuthorizationProvider.getLogin();
if (login == null) {
try {
GHMyself myself = fetch(GHMyself.class, "/user");
if (myselfConsumer != null) {
myselfConsumer.accept(myself);
}
login = myself.getLogin();
} catch (IOException e) {
return null;
}
} }
} }
this.login = login; return login;
} }
private <T> T fetch(Class<T> type, String urlPath) throws IOException { private <T> T fetch(Class<T> type, String urlPath) throws IOException {
@@ -202,7 +186,13 @@ abstract class GitHubClient {
* @return {@code true} if operations that require authentication will fail. * @return {@code true} if operations that require authentication will fail.
*/ */
public boolean isAnonymous() { public boolean isAnonymous() {
return login == null && encodedAuthorization == null; try {
return login == null && this.authorizationProvider.getEncodedAuthorization() == null;
} catch (IOException e) {
// An exception here means that the provider failed to provide authorization parameters,
// basically meaning the same as "no auth"
return false;
}
} }
/** /**
@@ -224,6 +214,11 @@ abstract class GitHubClient {
return getRateLimit(RateLimitTarget.NONE); return getRateLimit(RateLimitTarget.NONE);
} }
@CheckForNull
protected String getEncodedAuthorization() throws IOException {
return authorizationProvider.getEncodedAuthorization();
}
@Nonnull @Nonnull
GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException { GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {
GHRateLimit result; GHRateLimit result;
@@ -394,7 +389,6 @@ abstract class GitHubClient {
"GitHub API request [" + (login == null ? "anonymous" : login) + "]: " "GitHub API request [" + (login == null ? "anonymous" : login) + "]: "
+ request.method() + " " + request.url().toString()); + request.method() + " " + request.url().toString());
} }
rateLimitChecker.checkRateLimit(this, request); rateLimitChecker.checkRateLimit(this, request);
responseInfo = getResponseInfo(request); responseInfo = getResponseInfo(request);

View File

@@ -1,6 +1,7 @@
package org.kohsuke.github; package org.kohsuke.github;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.kohsuke.github.authorization.AuthorizationProvider;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -33,25 +34,19 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
class GitHubHttpUrlConnectionClient extends GitHubClient { class GitHubHttpUrlConnectionClient extends GitHubClient {
GitHubHttpUrlConnectionClient(String apiUrl, GitHubHttpUrlConnectionClient(String apiUrl,
String login,
String oauthAccessToken,
String jwtToken,
String password,
HttpConnector connector, HttpConnector connector,
RateLimitHandler rateLimitHandler, RateLimitHandler rateLimitHandler,
AbuseLimitHandler abuseLimitHandler, AbuseLimitHandler abuseLimitHandler,
GitHubRateLimitChecker rateLimitChecker, GitHubRateLimitChecker rateLimitChecker,
Consumer<GHMyself> myselfConsumer) throws IOException { Consumer<GHMyself> myselfConsumer,
AuthorizationProvider authorizationProvider) throws IOException {
super(apiUrl, super(apiUrl,
login,
oauthAccessToken,
jwtToken,
password,
connector, connector,
rateLimitHandler, rateLimitHandler,
abuseLimitHandler, abuseLimitHandler,
rateLimitChecker, rateLimitChecker,
myselfConsumer); myselfConsumer,
authorizationProvider);
} }
@Nonnull @Nonnull
@@ -114,8 +109,12 @@ class GitHubHttpUrlConnectionClient extends GitHubClient {
// if the authentication is needed but no credential is given, try it anyway (so that some calls // 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.) // that do work with anonymous access in the reduced form should still work.)
if (client.encodedAuthorization != null) if (!request.headers().containsKey("Authorization")) {
connection.setRequestProperty("Authorization", client.encodedAuthorization); String authorization = client.getEncodedAuthorization();
if (authorization != null) {
connection.setRequestProperty("Authorization", client.getEncodedAuthorization());
}
}
setRequestMethod(request.method(), connection); setRequestMethod(request.method(), connection);
buildRequest(request, connection); buildRequest(request, connection);

View File

@@ -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:
*
* <pre>
* {@code
* &#64;Override
* public String getEncodedAuthorization() {
* return "Bearer myBearerToken";
* }
* }
* </pre>
*
* @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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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:
*
* <pre>
* openssl pkcs8 -topk8 -inform PEM -outform DER -in ~/github-api-app.private-key.pem -out ~/github-api-app.private-key.der -nocrypt
* </pre>
*/
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();
}
}

View File

@@ -2,8 +2,12 @@ package org.kohsuke.github;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import org.apache.commons.io.IOUtils; 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.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyFactory; import java.security.KeyFactory;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec; 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_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 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) { private String createJwtToken(String keyFileResouceName, String appId) {
try { try {
String keyPEM = IOUtils.toString(this.getClass().getResource(keyFileResouceName), "US-ASCII") String keyPEM = IOUtils.toString(this.getClass().getResource(keyFileResouceName), "US-ASCII")
@@ -63,15 +84,15 @@ public class AbstractGHAppInstallationTest extends AbstractGitHubWireMockTest {
} }
protected GHAppInstallation getAppInstallationWithTokenApp1() throws IOException { 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 { 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 { protected GHAppInstallation getAppInstallationWithTokenApp3() throws IOException {
return getAppInstallationWithToken(createJwtToken(PRIVATE_KEY_FILE_APP_3, TEST_APP_ID_3)); return getAppInstallationWithToken(JWT_PROVIDER_3.getEncodedAuthorization());
} }
} }

View File

@@ -100,7 +100,6 @@ public abstract class AbstractGitHubWireMockTest extends Assert {
// This sets the user and password to a placeholder for wiremock testing // This sets the user and password to a placeholder for wiremock testing
// This makes the tests believe they are running with permissions // This makes the tests believe they are running with permissions
// The recorded stubs will behave like they running with permissions // The recorded stubs will behave like they running with permissions
builder.oauthToken = null;
builder.withPassword(STUBBED_USER_LOGIN, STUBBED_USER_PASSWORD); builder.withPassword(STUBBED_USER_LOGIN, STUBBED_USER_PASSWORD);
} }

View File

@@ -1,11 +1,14 @@
package org.kohsuke.github; package org.kohsuke.github;
import org.junit.Test; import org.junit.Test;
import org.kohsuke.github.authorization.UserAuthorizationProvider;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.*; import java.util.*;
import static org.hamcrest.CoreMatchers.*;
/** /**
* Unit test for {@link GitHub}. * Unit test for {@link GitHub}.
*/ */
@@ -56,19 +59,40 @@ public class GitHubConnectionTest extends AbstractGitHubWireMockTest {
Map<String, String> props = new HashMap<String, String>(); Map<String, String> props = new HashMap<String, String>();
props.put("login", "bogus"); props.put("endpoint", "bogus endpoint url");
props.put("oauth", "bogus"); props.put("oauth", "bogus oauth token string");
props.put("password", "bogus");
props.put("jwt", "bogus");
setupEnvironment(props); setupEnvironment(props);
GitHubBuilder builder = GitHubBuilder.fromEnvironment(); GitHubBuilder builder = GitHubBuilder.fromEnvironment();
assertEquals("bogus", builder.user); assertThat(builder.endpoint, equalTo("bogus endpoint url"));
assertEquals("bogus", builder.oauthToken);
assertEquals("bogus", builder.password); assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
assertEquals("bogus", builder.jwtToken); 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();
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();
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 class GitHubConnectionTest extends AbstractGitHubWireMockTest {
public void testGitHubBuilderFromCustomEnvironment() throws IOException { public void testGitHubBuilderFromCustomEnvironment() throws IOException {
Map<String, String> props = new HashMap<String, String>(); Map<String, String> props = new HashMap<String, String>();
props.put("customLogin", "bogusLogin"); props.put("customEndpoint", "bogus endpoint url");
props.put("customOauth", "bogusOauth"); props.put("customOauth", "bogus oauth token string");
props.put("customPassword", "bogusPassword");
props.put("customEndpoint", "bogusEndpoint");
setupEnvironment(props); setupEnvironment(props);
GitHubBuilder builder = GitHubBuilder GitHubBuilder builder = GitHubBuilder
.fromEnvironment("customLogin", "customPassword", "customOauth", "customEndpoint"); .fromEnvironment("customLogin", "customPassword", "customOauth", "customEndpoint");
assertEquals("bogusLogin", builder.user); assertThat(builder.endpoint, equalTo("bogus endpoint url"));
assertEquals("bogusOauth", builder.oauthToken);
assertEquals("bogusPassword", builder.password); assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
assertEquals("bogusEndpoint", builder.endpoint); 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 @Test
public void testGithubBuilderWithAppInstallationToken() throws Exception { public void testGithubBuilderWithAppInstallationToken() throws Exception {
GitHubBuilder builder = new GitHubBuilder().withAppInstallationToken("bogus");
assertEquals("bogus", builder.oauthToken); GitHubBuilder builder = new GitHubBuilder().withAppInstallationToken("bogus app token");
assertEquals("", builder.user); 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 // test authorization header is set as in the RFC6749
GitHub github = builder.build(); GitHub github = builder.build();
// change this to get a request // change this to get a request
assertEquals("token bogus", github.getClient().encodedAuthorization); assertEquals("token bogus app token", github.getClient().getEncodedAuthorization());
assertEquals("", github.getClient().login); assertEquals("", github.getClient().login);
} }

View File

@@ -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"));
}
}

View File

@@ -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:
*
* <pre>
* Authorization: Bearer ey{rest of the header}.{payload}.{signature}
* </pre>
*
* Matched by the regular expression:
*
* <pre>
* ^Bearer (?<JWTHeader>ey\S*)\.(?<JWTPayload>\S*)\.(?<JWTSignature>\S*)$
* </pre>
*
* 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();
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -0,0 +1,44 @@
{
"id": "bb7cf5bb-45b3-fba2-afd8-939b2c24787a",
"name": "app",
"request": {
"url": "/app",
"method": "GET",
"headers": {
"Authorization": {
"matches": "^Bearer (?<JWTHeader>ey\\S*)\\.(?<JWTPayload>\\S*)\\.(?<JWTSignature>\\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
}