Skip to content

feat: allow one app to authenticate #1053

New issue

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

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

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/main/java/org/kohsuke/github/GitHubClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ protected String getEncodedAuthorization() throws IOException {
return authorizationProvider.getEncodedAuthorization();
}

@CheckForNull
protected String getEncodedAuthorization(URL url) throws IOException {
return authorizationProvider.getEncodedAuthorization(url);
}

@Nonnull
GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {
GHRateLimit result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ 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 (!request.headers().containsKey("Authorization")) {
String authorization = client.getEncodedAuthorization();
String authorization = client.getEncodedAuthorization(request.url());
if (authorization != null) {
connection.setRequestProperty("Authorization", client.getEncodedAuthorization());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.kohsuke.github.authorization;

import java.io.IOException;
import java.net.URL;

/**
* Provides a functional interface that returns a valid encodedAuthorization. This strategy allows for a provider that
Expand Down Expand Up @@ -30,13 +31,18 @@ public interface AuthorizationProvider {
* on any error that prevents the provider from getting a valid authorization
*/
String getEncodedAuthorization() throws IOException;
String getEncodedAuthorization(URL url) throws IOException;

/**
* A {@link AuthorizationProvider} that ensures that no credentials are returned
*/
class AnonymousAuthorizationProvider implements AuthorizationProvider {
@Override
public String getEncodedAuthorization() throws IOException {
return getEncodedAuthorization(null);
}
@Override
public String getEncodedAuthorization(URL url) throws IOException {
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.kohsuke.github.authorization;

import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

Expand Down Expand Up @@ -92,10 +93,15 @@ public static AuthorizationProvider fromLoginAndPassword(String login, String pa
}

@Override
public String getEncodedAuthorization() {
public String getEncodedAuthorization(URL url) {
return this.authorization;
}

@Override
public String getEncodedAuthorization() {
return getEncodedAuthorization(null);
}

/**
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@
import org.kohsuke.github.GitHub;

import java.io.IOException;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nonnull;

Expand All @@ -17,47 +23,75 @@
*/
public class OrgAppInstallationAuthorizationProvider extends GitHub.DependentAuthorizationProvider {

private final String organizationName;
private static final Pattern pattern = Pattern.compile("/repos/(.*)/.*");
Copy link
Member

@bitwiseman bitwiseman Mar 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This provider has a reference to a GitHub instance, meaning we can get the base URL using getApiUrl() and then do simpler string matching. I'd rather not regex if we don't have to.

Also do GitHub App tokens only apply to /repos/* endpoints or can they interact with other endpoints? I'm pretty sure there are even if they are not accessed by Jenkins.


private String latestToken;
private Map<String, String> latestToken = new HashMap<>();

@Nonnull
private Instant validUntil = Instant.MIN;
private Map<String, Instant> validUntil = new HashMap<>();

/**
* 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) {
public OrgAppInstallationAuthorizationProvider(AuthorizationProvider authorizationProvider) {
super(authorizationProvider);
this.organizationName = organizationName;
}

@Override
public String getEncodedAuthorization() throws IOException {
public String getEncodedAuthorization(URL url) throws IOException {
synchronized (this) {
if (latestToken == null || Instant.now().isAfter(this.validUntil)) {
refreshToken();
String org = getOrgFromURL(url);
if (latestToken.get(org) == null || this.validUntil.get(org) == null
|| Instant.now().isAfter(this.validUntil.get(org))) {
refreshToken(url);
}
return String.format("token %s", latestToken.get(org));
}
}

@Override
public String getEncodedAuthorization() throws IOException {
return getEncodedAuthorization(null);
}

/**
* Try to figure out what org is this url trying to access so we can use the correct App installation for that org.
*
* @param url
* @return the organization or "" if it cannot be computed
*/
private String getOrgFromURL(URL url) {
if (url != null) {
Matcher matcher = pattern.matcher(url.getPath());
if (matcher.matches()) {
return matcher.group(1);
}
return String.format("token %s", latestToken);
}
return "";
}

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());
private void refreshToken(URL url) throws IOException {
List<GHAppInstallation> installations = this.gitHub().getApp().listInstallations().asList();
// take the first one if no one matches
GHAppInstallation installation = installations.get(0);
String org = getOrgFromURL(url);
for (GHAppInstallation ghAppInstallation : installations) {
if (org.equals(installation.getAccount().getLogin())) {
System.out.println(
String.format("Found installation for path %s: %s", url.getPath(), installation.getHtmlUrl()));
installation = ghAppInstallation;
break;
}
}
GHAppInstallationToken ghAppInstallationToken = installation.createToken().create();
this.validUntil.put(org, ghAppInstallationToken.getExpiresAt().toInstant().minus(Duration.ofMinutes(5)));
this.latestToken.put(org, Objects.requireNonNull(ghAppInstallationToken.getToken()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
Expand Down Expand Up @@ -60,6 +61,11 @@ public JWTTokenProvider(String applicationId, PrivateKey privateKey) {

@Override
public String getEncodedAuthorization() throws IOException {
return getEncodedAuthorization(null);
}

@Override
public String getEncodedAuthorization(URL url) throws IOException {
synchronized (this) {
if (Instant.now().isAfter(validUntil)) {
token = refreshJWT();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.kohsuke.github.authorization.OrgAppInstallationAuthorizationProvider;

import java.io.IOException;
import java.net.URL;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.notNullValue;
Expand All @@ -18,7 +19,6 @@ public OrgAppInstallationAuthorizationProviderTest() {
@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())
Expand All @@ -29,12 +29,13 @@ public void invalidJWTTokenRaisesException() throws IOException {

@Test
public void validJWTTokenAllowsOauthTokenRequest() throws IOException {
OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider("hub4j-test-org",
OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider(
ImmutableAuthorizationProvider.fromJwtToken("bogus-valid-token"));
gitHub = getGitHubBuilder().withAuthorizationProvider(provider)
.withEndpoint(mockGitHub.apiServer().baseUrl())
.build();
String encodedAuthorization = provider.getEncodedAuthorization();
String encodedAuthorization = provider
.getEncodedAuthorization(new URL("https://api.github.com/repos/hub4j-test-org/github-api"));

assertThat(encodedAuthorization, notNullValue());
assertThat(encodedAuthorization, equalTo("token v1.9a12d913f980a45a16ac9c3a9d34d9b7sa314cb6"));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
[{
"id": 11575015,
"account": {
"login": "hub4j-test-org",
Expand Down Expand Up @@ -40,4 +40,4 @@
"single_file_name": null,
"suspended_by": null,
"suspended_at": null
}
}]
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "9ffe1e34-1d0e-495a-abdc-86fdf1d15334",
"name": "orgs_hub4j-test-org_installation",
"request": {
"url": "/orgs/hub4j-test-org/installation",
"url": "/app/installations",
"method": "GET",
"headers": {
"Accept": {
Expand Down