Skip to content
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

Skip refresh GitHub tokens, override refresh Azure DevOps token request #699

Merged
merged 2 commits into from
Jul 25, 2024
Merged
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
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
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
Loading