Skip to content

Commit

Permalink
Skip refresh GitHub tokens, override refresh Azure DevOps token request
Browse files Browse the repository at this point in the history
  • Loading branch information
vinokurig committed Jul 11, 2024
1 parent 14d13b6 commit 38e3a01
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 17 deletions.
8 changes: 8 additions & 0 deletions wsmaster/che-core-api-auth-azure-devops/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
Expand Down Expand Up @@ -55,6 +59,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-auth-shared</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-dto</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-annotations</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@
*/
package org.eclipse.che.security.oauth;

import static java.lang.String.format;
import static java.net.URLEncoder.encode;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.che.commons.json.JsonHelper.fromJson;
import static org.eclipse.che.commons.lang.StringUtils.trimEnd;
import static org.eclipse.che.dto.server.DtoFactory.newDto;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
import com.google.api.client.auth.oauth2.AuthorizationCodeTokenRequest;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.util.store.MemoryDataStoreFactory;
import com.google.common.io.CharStreams;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.net.http.HttpClient;
Expand All @@ -26,7 +35,6 @@
import java.util.List;
import javax.inject.Singleton;
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
import org.eclipse.che.commons.json.JsonHelper;
import org.eclipse.che.commons.json.JsonParseException;

/**
Expand All @@ -39,10 +47,14 @@ public class AzureDevOpsOAuthAuthenticator extends OAuthAuthenticator {
private final String azureDevOpsScmApiEndpoint;
private final String cheApiEndpoint;
private final String azureDevOpsUserProfileDataApiUrl;
private final String tokenUri;
private final String[] redirectUris;
private final String API_VERSION = "7.0";
private final String PROVIDER_NAME = "azure-devops";
private final String clientSecret;

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

public AzureDevOpsOAuthAuthenticator(
String cheApiEndpoint,
String clientId,
Expand All @@ -57,9 +69,11 @@ public AzureDevOpsOAuthAuthenticator(
this.clientSecret = clientSecret;
this.azureDevOpsScmApiEndpoint = trimEnd(azureDevOpsScmApiEndpoint, '/');
this.azureDevOpsUserProfileDataApiUrl =
String.format(
format(
"%s/_apis/profile/profiles/me?api-version=%s",
trimEnd(azureDevOpsApiEndpoint, '/'), API_VERSION);
this.tokenUri = tokenUri;
this.redirectUris = redirectUris;
configure(
clientId, clientSecret, redirectUris, authUri, tokenUri, new MemoryDataStoreFactory());
}
Expand All @@ -74,7 +88,7 @@ public AzureDevOpsOAuthAuthenticator(
public String getAuthenticateUrl(URL requestUrl, List<String> scopes) {
AuthorizationCodeRequestUrl url = flow.newAuthorizationUrl().setScopes(scopes);
url.set("response_type", "Assertion");
url.set("redirect_uri", String.format("%s/oauth/callback", cheApiEndpoint));
url.set("redirect_uri", format("%s/oauth/callback", cheApiEndpoint));
url.setState(prepareState(requestUrl));
return url.build();
}
Expand Down Expand Up @@ -116,12 +130,64 @@ private AzureDevOpsUserProfile getUserProfile(String accessToken)
try {
HttpResponse<InputStream> response =
client.send(request, HttpResponse.BodyHandlers.ofInputStream());
return JsonHelper.fromJson(response.body(), AzureDevOpsUserProfile.class, null);
return fromJson(response.body(), AzureDevOpsUserProfile.class, null);
} catch (IOException | InterruptedException | JsonParseException e) {
throw new OAuthAuthenticationException(e.getMessage(), e);
}
}

private HttpRequest.BodyPublisher getParamsUrlEncoded(String refreshToken) {
String urlEncoded =
format(
"client_assertion_type=%1s&"
+ "client_assertion=%2s&"
+ "grant_type=refresh_token&"
+ "assertion=%3s&"
+ "redirect_uri=%4s",
encode("urn:ietf:params:oauth:client-assertion-type:jwt-bearer", UTF_8),
encode(clientSecret, UTF_8),
refreshToken,
redirectUris[0]);
return HttpRequest.BodyPublishers.ofString(urlEncoded);
}

/**
* Refresh personal access token.
*
* @param userId user identifier
* @return a refreshed token object or the previous token if the refresh failed
* @throws IOException when error occurs during token loading
*/
public OAuthToken refreshToken(String userId) throws IOException {
if (!isConfigured()) {
throw new IOException(AUTHENTICATOR_IS_NOT_CONFIGURED);
}

Credential credential = flow.loadCredential(userId);
if (credential == null) {
return null;
}
HttpClient client = HttpClient.newHttpClient();
HttpRequest request =
HttpRequest.newBuilder(URI.create(tokenUri))
.POST(getParamsUrlEncoded(credential.getRefreshToken()))
.headers("Content-Type", "application/x-www-form-urlencoded")
.build();
try {
HttpResponse<InputStream> response =
client.send(request, HttpResponse.BodyHandlers.ofInputStream());
AzureDevOpsRefreshToken token =
OBJECT_MAPPER.readValue(
CharStreams.toString(new InputStreamReader(response.body(), UTF_8)),
AzureDevOpsRefreshToken.class);
String accessToken = token.getAccessToken();
credential.setAccessToken(accessToken);
return newDto(OAuthToken.class).withToken(accessToken);
} catch (IOException | InterruptedException exception) {
return newDto(OAuthToken.class).withToken(credential.getAccessToken());
}
}

/**
* Returns the token request. Overrides the default implementation to set the {@code grant_type},
* {@code assertion}, {@code client_assertion} and {@code client_assertion_type} accordingly to
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2012-2024 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.security.oauth;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Objects;

@JsonIgnoreProperties(ignoreUnknown = true)
public class AzureDevOpsRefreshToken {
/** Access token issued by the authorization server. */
private String accessToken;

/** Token type. */
private String tokenType;

/** Refresh token which can be used to obtain new access tokens. */
private String refreshToken;

/**
* Lifetime in seconds of the access token (for example 3600 for an hour) or {@code null} for
* none.
*/
private String expiresInSeconds;

/** Scope of the access token. */
private String scope;

public String getAccessToken() {
return accessToken;
}

public String getTokenType() {
return tokenType;
}

public String getRefreshToken() {
return refreshToken;
}

public String getScope() {
return scope;
}

public String getExpiresInSeconds() {
return expiresInSeconds;
}

@JsonProperty("access_token")
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}

@JsonProperty("token_type")
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}

@JsonProperty("refresh_token")
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

@JsonProperty("expires_in")
public void setExpiresInSeconds(String expiresInSeconds) {
this.expiresInSeconds = expiresInSeconds;
}

public void setScope(String scope) {
this.scope = scope;
}

@Override
public String toString() {
return "AzureDevOpsRefreshToken{"
+ "accessToken='"
+ accessToken
+ '\''
+ ", tokenType='"
+ tokenType
+ '\''
+ ", refreshToken='"
+ refreshToken
+ '\''
+ ", expiresInSeconds='"
+ expiresInSeconds
+ '\''
+ ", scope='"
+ scope
+ '\''
+ '}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AzureDevOpsRefreshToken that = (AzureDevOpsRefreshToken) o;
return Objects.equals(accessToken, that.accessToken)
&& Objects.equals(tokenType, that.tokenType)
&& Objects.equals(refreshToken, that.refreshToken)
&& Objects.equals(expiresInSeconds, that.expiresInSeconds)
&& Objects.equals(scope, that.scope);
}

@Override
public int hashCode() {
return Objects.hash(accessToken, tokenType, refreshToken, expiresInSeconds, scope);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

/** Authentication service which allow get access token from OAuth provider site. */
public abstract class OAuthAuthenticator {
private static final String AUTHENTICATOR_IS_NOT_CONFIGURED = "Authenticator is not configured";
protected static final String AUTHENTICATOR_IS_NOT_CONFIGURED = "Authenticator is not configured";

private static final Logger LOG = LoggerFactory.getLogger(OAuthAuthenticator.class);

Expand Down Expand Up @@ -362,7 +362,7 @@ public OAuthToken refreshToken(String userId) throws IOException {
*
* @param userId user
*/
private void invalidateTokenByUser(String userId) throws IOException {
protected void invalidateTokenByUser(String userId) throws IOException {
Credential credential = flow.loadCredential(userId);
if (credential != null) {
flow.getCredentialDataStore().delete(userId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,29 +127,22 @@ public abstract class AbstractGithubPersonalAccessTokenFetcher

public PersonalAccessToken refreshPersonalAccessToken(Subject cheSubject, String scmServerUrl)
throws ScmUnauthorizedException, ScmCommunicationException, UnknownScmProviderException {
return getOrRefreshPersonalAccessToken(cheSubject, scmServerUrl, true);
// Tokens generated via GitHub OAuth app do not have an expiration date, so we don't need to
// refresh them.
return fetchPersonalAccessToken(cheSubject, scmServerUrl);
}

@Override
public PersonalAccessToken fetchPersonalAccessToken(Subject cheSubject, String scmServerUrl)
throws ScmUnauthorizedException, ScmCommunicationException, UnknownScmProviderException {
return getOrRefreshPersonalAccessToken(cheSubject, scmServerUrl, false);
}

private PersonalAccessToken getOrRefreshPersonalAccessToken(
Subject cheSubject, String scmServerUrl, boolean forceRefreshToken)
throws ScmUnauthorizedException, ScmCommunicationException, UnknownScmProviderException {
OAuthToken oAuthToken;

if (githubApiClient == null || !githubApiClient.isConnected(scmServerUrl)) {
LOG.debug("not a valid url {} for current fetcher ", scmServerUrl);
return null;
}
try {
oAuthToken =
forceRefreshToken
? oAuthAPI.refreshToken(providerName)
: oAuthAPI.getOrRefreshToken(providerName);
oAuthToken = oAuthAPI.getOrRefreshToken(providerName);
String tokenName = NameGenerator.generate(OAUTH_2_PREFIX, 5);
String tokenId = NameGenerator.generate("id-", 5);
Optional<Pair<Boolean, String>> valid =
Expand Down

0 comments on commit 38e3a01

Please sign in to comment.