Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Collections;
import java.util.List;
import javax.inject.Singleton;
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
Expand All @@ -55,6 +56,7 @@ public class AzureDevOpsOAuthAuthenticator extends OAuthAuthenticator {
private final String PROVIDER_NAME = "azure-devops";
private final String clientId;
private final String clientSecret;
private final boolean isDevOpsOauth;

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

Expand All @@ -66,7 +68,8 @@ public AzureDevOpsOAuthAuthenticator(
String azureDevOpsScmApiEndpoint,
String authUri,
String tokenUri,
String[] redirectUris)
String[] redirectUris,
boolean isDevOpsOauth)
throws IOException {
this.cheApiEndpoint = cheApiEndpoint;
this.clientId = clientId;
Expand All @@ -78,6 +81,7 @@ public AzureDevOpsOAuthAuthenticator(
trimEnd(azureDevOpsApiEndpoint, '/'), API_VERSION);
this.tokenUri = tokenUri;
this.redirectUris = redirectUris;
this.isDevOpsOauth = isDevOpsOauth;
configure(
clientId, clientSecret, redirectUris, authUri, tokenUri, new MemoryDataStoreFactory());
}
Expand All @@ -90,8 +94,15 @@ public AzureDevOpsOAuthAuthenticator(
*/
@Override
public String getAuthenticateUrl(URL requestUrl, List<String> scopes) {
if (isDevOpsOauth) {
scopes = Collections.singletonList("vso.code_write");
}
AuthorizationCodeRequestUrl url = flow.newAuthorizationUrl().setScopes(scopes);
url.set("response_type", "code");
if (isDevOpsOauth) {
url.set("response_type", "Assertion");
} else {
url.set("response_type", "code");
}
url.set("redirect_uri", format("%s/oauth/callback", cheApiEndpoint));
url.setState(prepareState(requestUrl));
return url.build();
Expand Down Expand Up @@ -204,10 +215,19 @@ protected AuthorizationCodeTokenRequest getAuthorizationCodeTokenRequest(
URL requestUrl, List<String> scopes, String code) {
AuthorizationCodeTokenRequest request =
super.getAuthorizationCodeTokenRequest(requestUrl, scopes, code);
request.set("client_id", clientId);
request.set("grant_type", "authorization_code");
request.set("client_secret", URLEncoder.encode(clientSecret));
request.setResponseClass(TokenResponse.class);
if (isDevOpsOauth) {
request.set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
request.set("assertion", code);
request.set("client_assertion", clientSecret);
request.set(
"client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
request.setResponseClass(AzureDevOpsTokenResponse.class);
} else {
request.set("client_id", clientId);
request.set("grant_type", "authorization_code");
request.set("client_secret", URLEncoder.encode(clientSecret));
request.setResponseClass(TokenResponse.class);
}
return request;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,34 @@ private OAuthAuthenticator getOAuthAuthenticator(
if (!isNullOrEmpty(clientIdPath)
&& !isNullOrEmpty(clientSecretPath)
&& !isNullOrEmpty(tenantIdPath)) {
final String tenantId = Files.readString(Path.of(tenantIdPath)).trim();
// This flag is needed to support the deprecated Azure DevOps oauth apps.
// TODO remove the related logic when the deprecated Azure DevOps oauth app is no longer
// available, see
// https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops#azure-devops-oauth-deprecated
boolean isDevOpsOauth = false;
String tenantId = null;
try {
tenantId = Files.readString(Path.of(tenantIdPath)).trim();
} catch (IOException e) {
isDevOpsOauth = true;
}
final String clientId = Files.readString(Path.of(clientIdPath)).trim();
final String clientSecret = Files.readString(Path.of(clientSecretPath)).trim();
if (!isNullOrEmpty(clientId) && !isNullOrEmpty(clientSecret) && !isNullOrEmpty(tenantId)) {
if (!isNullOrEmpty(clientId) && !isNullOrEmpty(clientSecret)) {
return new AzureDevOpsOAuthAuthenticator(
cheApiEndpoint,
clientId,
clientSecret,
azureDevOpsApiEndpoint,
azureDevOpsScmApiEndpoint,
String.format(authUriTemplate, tenantId),
String.format(tokenUriTemplate, tenantId),
redirectUris);
isDevOpsOauth
? "https://app.vssps.visualstudio.com/oauth2/authorize"
: String.format(authUriTemplate, tenantId),
isDevOpsOauth
? "https://app.vssps.visualstudio.com/oauth2/authorize"
: String.format(tokenUriTemplate, tenantId),
redirectUris,
isDevOpsOauth);
}
}
return new NoopOAuthAuthenticator();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2012-2026 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.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.json.JsonString;
import com.google.api.client.util.Key;

/**
* The only difference between from {@link TokenResponse} is that {@link #expiresInSeconds} field is
* represented in a {@link String} format.
*
* <p>https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops#response---authorize-app
*
* @author Anatolii Bazko
*/
public class AzureDevOpsTokenResponse extends TokenResponse {
@JsonString
@Key("expires_in")
private Long expiresInSeconds;

public Long getExpiresInSeconds() {
return expiresInSeconds;
}

public AzureDevOpsTokenResponse setExpiresInSeconds(Long expiresInSeconds) {
this.expiresInSeconds = expiresInSeconds;
return this;
}
}
Loading