-
Notifications
You must be signed in to change notification settings - Fork 737
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
Feat/credential provider refresh #945
Feat/credential provider refresh #945
Conversation
This is basically a class that will hold an authorization string, returning the same value all the time
@bitwiseman Hi Liam! As I see it, the logic that the user will need to follow is:
We will need to either implement a JWTokenProvider in a similar fashion, leaving the user to provide a valid JWT token to use in a CredentialProvider, or alternatively implement all the logic in the library. I am going to do the first approach for now! |
If I understand correctly, we'll need to do the second approach in order for refresh to work for longer than the expiration time of the JWT Token. And JWT Tokens have a shorter life span than the GitHub App Tokens. |
Sorry, bad explanation. As I see it right now we have 2 choices:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
</dependency> Will address the other issues, thank you for your time and insight! |
This is basically an implementation of a CredentialProvider that will always authenticate anonymously
These methods let us build the most-used cases for static credentials that will never change: - JWT credentials - Token-based credentials - Basic Auth credentials
With this we also need to check for exceptions when calling "/user", because now we don't know what kind of credentials are coming from the provider, and we could be requesting a "/user" when the type of credentials is not supported
The JWTTokenProvider implementation is left to the end user, otherwise we would need to include specific libraries, at least as far as I am aware. The OrgInstallationCredentialProvider will give a token, refreshing when necessary and using the JWTTokenProvider that the user needs to provide to request new tokens
As it's right now, an example on how to use this will be as follows:
// User is left with the responsibility to make sure that the passed credential provider always returns
// a valid JWT token
GitHub jwtAuthenticated = new GitHubBuilder().withCredentialProvider(() -> "a valid JWT token").build();
// With the JWT authenticated client, we will be requesting installation apps
GitHub gh = new GitHubBuilder()
.withCredentialProvider(new OrgInstallationCredentialProvider("myOrganization", jwtAuthenticated))
.build(); Where OrgInstallationCredentialProvider is an Oauth2 Token provider that uses a JWT authenticated client. With the JWT token, we request a new installation token when the current one has expired. Users need to provide valid tokens. E.g: @Slf4j
@Component
@RequiredArgsConstructor
public class JWTTokens {
private static final long MINUTES_10 = Duration.buildByMinutes(10).getMilliseconds();
private final PrivateKey privateKey;
public String token() {
log.debug("Getting a new JWT token");
return getJWT();
}
public String getJWT() {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// Let's set the JWT Claims
JwtBuilder builder = Jwts.builder()
.setIssuedAt(now)
.setIssuer("myApplicationId")
.signWith(privateKey, SignatureAlgorithm.RS256);
// if it has been specified, let's add the expiration
if (MINUTES_10 > 0) {
long expMillis = nowMillis + MINUTES_10;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
// Builds the JWT and serializes it to a compact, URL-safe string
return builder.compact();
}
} We could simplify this and provide the creation of the JWT token from a given private key and applicationId, but it would mean to add dependencies to the library. I think that it's better to let users chose what kind of JWT libraries they use. We are of course missing tests. Ready to review! |
4c16eaa
to
6670446
Compare
@MarcosCela |
Definetly, I will take a look as soon as I can, thanks again for your time and dedication!! |
This looks extremely promising, to perform automatic access token refresh, I can do something like this: CredentialProvider prov = new JWTTokenProvider("applicationId", new File("/my/path.extension"));
GitHub jwtTokenRequester = new GitHubBuilder()
.withCredentialProvider(prov)
.build();
GitHub gh = new GitHubBuilder().withCredentialProvider(() -> {
// Missing all the refresh token logic, this is requesting a new token all the time (for simplicity)
return jwtTokenRequester.getApp().getInstallationById("installationId").getAccessTokenUrl();
}).build();
// From now on, I will just use "gh" for the rest of my program logic, that defers to the inlined
// credential provider, that uses a second GitHub instance "jwtTokenRequester" to refresh the access token
// sounds convoluted With your changes I think that the client is extremely flexible in terms of authentication, looks finished to me! |
I wouldn't use this as code as the example. I really don't want people to do it the way you've shown above. I want them to implement the CredentialProvider jwtProvider = new JWTTokenProvider("applicationId", new File("/my/path.extension"));
GitHub gh = new GitHubBuilder().withCredentialProvider(new OrgInstallationCredentialProvider("testOrganization", jwtProvider)).build(); Oh, we need to change Also, what do you think of changing |
100% agree on the inlining, it should have a different interface/class class. I will proceed to rename the name to AuthorizationProvider, and all implementations if required (e.g: AppInstallationAuthorizationProvider). |
This includes renames in comments, related methods, javadocs and fields/variables.
…orizationProvider
I applied your changes and tested it (successfully!) with the following snippet: AuthorizationProvider jwt = new JWTTokenProvider("12345",
Paths.get("/location/to/my/key"));
OrgAppInstallationAuthorizationProvider orgAUth =
new OrgAppInstallationAuthorizationProvider("my-organization", jwt);
GitHub gh = new GitHubBuilder().withAuthorizationProvider(orgAUth)
.build();
System.out.println("gh.getRateLimit() = " + gh.getRateLimit()); Is this the way that you were referring to? |
src/main/java/org/kohsuke/github/extras/auth/JWTTokenProvider.java
Outdated
Show resolved
Hide resolved
} | ||
|
||
protected GHAppInstallation getAppInstallationWithTokenApp3() throws IOException { | ||
return getAppInstallationWithToken(createJwtToken(PRIVATE_KEY_FILE_APP_3, TEST_APP_ID_3)); | ||
return getAppInstallationWithToken(JWT_PROVIDER_3.getEncodedAuthorization()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MarcosCela
To test JWT provider, maybe just add an assertion to the test that uses these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests added, what do you think?
This test basically ensures that the requests made with a JWTTokenProvider follow a valid Authentication pattern, verifying that the header "conforms" to a valid JWT token More information on JWT tokens can be found at: - https://jwt.io/introduction/
* <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice.
Rather than exposing an unsafe wrapper for GitHub instances, I added a base class that can be extended by anyone wanting to implement an authorization provider that needs a GitHub instance to generate it's authorization string.
* @throws GeneralSecurityException | ||
* if we couldn't parse the string | ||
*/ | ||
private static PrivateKey getPrivateKeyFromString(final String key) throws GeneralSecurityException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reason this is private? seems like I just have to re-implement this / copy it if I'm working with strings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably better to create a constructor that takes a String
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes or that...
Description
This PR attempts to solve: #941