GitLab OAuth authentication with embededOAuthAPI (#195)

GitLab OAuth authentication with embededOAuthAPI

Signed-off-by: Pavol Baran <pbaran@redhat.com>
pull/248/head
Pavol Baran 2022-01-26 11:06:57 +01:00 committed by GitHub
parent 0189b2866a
commit c9f724bb8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 501 additions and 0 deletions

View File

@ -103,6 +103,10 @@
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-auth-bitbucket</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-auth-gitlab</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-auth-openshift</artifactId>

View File

@ -105,6 +105,7 @@ import org.eclipse.che.multiuser.resource.api.ResourceModule;
import org.eclipse.che.security.PBKDF2PasswordEncryptor;
import org.eclipse.che.security.PasswordEncryptor;
import org.eclipse.che.security.oauth.EmbeddedOAuthAPI;
import org.eclipse.che.security.oauth.GitLabModule;
import org.eclipse.che.security.oauth.OAuthAPI;
import org.eclipse.che.security.oauth.OpenShiftOAuthModule;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientConfigFactory;
@ -284,6 +285,7 @@ public class WsMasterModule extends AbstractModule {
install(new FactoryModuleBuilder().build(PassThroughProxyProvisionerFactory.class));
installDefaultSecureServerExposer(infrastructure);
install(new org.eclipse.che.security.oauth1.BitbucketModule());
install(new GitLabModule());
configureMultiUserMode(persistenceProperties, infrastructure);

View File

@ -767,3 +767,10 @@ che.integration.gitlab.server_endpoints=NULL
# Address of the GitLab server with configured OAuth 2 integration
che.integration.gitlab.oauth_endpoint=NULL
# Configuration of GitLab OAuth2 client. Used to obtain Personal access tokens.
# Location of the file with GitLab client id.
che.oauth2.gitlab.clientid_filepath=NULL
# Location of the file with GitLab client secret.
che.oauth2.gitlab.clientsecret_filepath=NULL

View File

@ -661,6 +661,11 @@
<artifactId>che-core-api-auth-github</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-auth-gitlab</artifactId>
<version>${che.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-auth-openshift</artifactId>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2012-2021 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
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>che-master-parent</artifactId>
<groupId>org.eclipse.che.core</groupId>
<version>7.42.0-SNAPSHOT</version>
</parent>
<artifactId>che-core-api-auth-gitlab</artifactId>
<packaging>jar</packaging>
<name>Che Core :: API :: Authentication GitLab</name>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client</artifactId>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-auth</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-auth-shared</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-commons-json</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2012-2021 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.inject.AbstractModule;
import com.google.inject.multibindings.Multibinder;
/**
* Setup GitlabOAuthAuthenticator in guice container.
*
* @author Pavol Baran
*/
public class GitLabModule extends AbstractModule {
@Override
protected void configure() {
Multibinder<OAuthAuthenticator> oAuthAuthenticators =
Multibinder.newSetBinder(binder(), OAuthAuthenticator.class);
oAuthAuthenticators.addBinding().toProvider(GitLabOAuthAuthenticatorProvider.class);
}
}

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) 2012-2021 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 static com.google.common.base.Strings.isNullOrEmpty;
import com.google.api.client.util.store.MemoryDataStoreFactory;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
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;
import org.eclipse.che.security.oauth.shared.User;
/**
* OAuth2 authenticator for GitLab account.
*
* @author Pavol Baran
*/
@Singleton
public class GitLabOAuthAuthenticator extends OAuthAuthenticator {
private final String gitlabUserEndpoint;
private final String cheApiEndpoint;
public GitLabOAuthAuthenticator(
String clientId, String clientSecret, String gitlabEndpoint, String cheApiEndpoint)
throws IOException {
this.gitlabUserEndpoint = gitlabEndpoint + "/api/v4/user";
this.cheApiEndpoint = cheApiEndpoint;
configure(
clientId,
clientSecret,
new String[] {},
gitlabEndpoint + "/oauth/authorize",
gitlabEndpoint + "/oauth/token",
new MemoryDataStoreFactory());
}
@Override
public User getUser(OAuthToken accessToken) throws OAuthAuthenticationException {
GitLabUser user = getJson(gitlabUserEndpoint, accessToken.getToken(), GitLabUser.class);
final String email = user.getEmail();
if (isNullOrEmpty(email)) {
throw new OAuthAuthenticationException(
"Sorry, we failed to find any verified email associated with your GitLab account."
+ " Please, verify at least one email in your account and try to connect with GitLab again.");
}
try {
new InternetAddress(email).validate();
} catch (AddressException e) {
throw new OAuthAuthenticationException(e.getMessage());
}
return user;
}
@Override
public String getOAuthProvider() {
return "gitlab";
}
@Override
protected String findRedirectUrl(URL requestUrl) {
return cheApiEndpoint + "/oauth/callback";
}
@Override
protected <O> O getJson(String getUserUrl, String accessToken, Class<O> userClass)
throws OAuthAuthenticationException {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request =
HttpRequest.newBuilder(URI.create(getUserUrl))
.header("Authorization", "Bearer " + accessToken)
.build();
try {
HttpResponse<InputStream> response =
client.send(request, HttpResponse.BodyHandlers.ofInputStream());
return JsonHelper.fromJson(response.body(), userClass, null);
} catch (IOException | InterruptedException | JsonParseException e) {
throw new OAuthAuthenticationException(e.getMessage(), e);
}
}
@Override
public OAuthToken getToken(String userId) throws IOException {
final OAuthToken token = super.getToken(userId);
try {
if (token == null
|| token.getToken() == null
|| token.getToken().isEmpty()
|| getJson(gitlabUserEndpoint, token.getToken(), GitLabUser.class) == null) {
return null;
}
} catch (OAuthAuthenticationException e) {
return null;
}
return token;
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2012-2021 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 static com.google.common.base.Strings.isNullOrEmpty;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.eclipse.che.api.auth.shared.dto.OAuthToken;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.security.oauth.shared.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides implementation of GitLab {@link OAuthAuthenticator} based on available configuration.
*
* @author Pavol Baran
*/
@Singleton
public class GitLabOAuthAuthenticatorProvider implements Provider<OAuthAuthenticator> {
private static final Logger LOG = LoggerFactory.getLogger(GitLabOAuthAuthenticatorProvider.class);
private final OAuthAuthenticator authenticator;
@Inject
public GitLabOAuthAuthenticatorProvider(
@Nullable @Named("che.oauth2.gitlab.clientid_filepath") String clientIdPath,
@Nullable @Named("che.oauth2.gitlab.clientsecret_filepath") String clientSecretPath,
@Nullable @Named("che.integration.gitlab.oauth_endpoint") String gitlabEndpoint,
@Named("che.api") String cheApiEndpoint)
throws IOException {
authenticator =
getOAuthAuthenticator(clientIdPath, clientSecretPath, gitlabEndpoint, cheApiEndpoint);
LOG.debug("{} GitLab OAuth Authenticator is used.", authenticator);
}
@Override
public OAuthAuthenticator get() {
return authenticator;
}
private OAuthAuthenticator getOAuthAuthenticator(
String clientIdPath, String clientSecretPath, String gitlabEndpoint, String cheApiEndpoint)
throws IOException {
if (!isNullOrEmpty(clientIdPath)
&& !isNullOrEmpty(clientSecretPath)
&& !isNullOrEmpty(gitlabEndpoint)) {
String clientId = Files.readString(Path.of(clientIdPath));
String clientSecret = Files.readString(Path.of(clientSecretPath));
if (!isNullOrEmpty(clientId) && !isNullOrEmpty(clientSecret)) {
return new GitLabOAuthAuthenticator(clientId, clientSecret, gitlabEndpoint, cheApiEndpoint);
}
}
return new NoopOAuthAuthenticator();
}
static class NoopOAuthAuthenticator extends OAuthAuthenticator {
@Override
public User getUser(OAuthToken accessToken) throws OAuthAuthenticationException {
throw new OAuthAuthenticationException(
"The fallback noop authenticator cannot be used for GitLab authentication. Make sure OAuth is properly configured.");
}
@Override
public String getOAuthProvider() {
return "Noop";
}
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2012-2021 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 org.eclipse.che.security.oauth.shared.User;
/**
* Represents GitLab user.
*
* @author Pavol Baran
*/
public class GitLabUser implements User {
private String id;
private String name;
private String email;
@Override
public String getId() {
return id;
}
@Override
public void setId(String id) {
this.id = id;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public String getEmail() {
return email;
}
@Override
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "GitLabUser{"
+ "id='"
+ id
+ '\''
+ ", name='"
+ name
+ '\''
+ ", email='"
+ email
+ '\''
+ '}';
}
}

View File

@ -0,0 +1,108 @@
/*
* Copyright (c) 2012-2021 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 static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class GitLabOAuthAuthenticatorProviderTest {
private static final String TEST_URI = "https://gitlab.com";
private File credentialFile;
private File emptyFile;
@BeforeClass
public void setup() throws IOException {
credentialFile = File.createTempFile("GitLabOAuthAuthenticatorProviderTest-", "-credentials");
Files.asCharSink(credentialFile, Charset.defaultCharset()).write("id/secret");
credentialFile.deleteOnExit();
emptyFile = File.createTempFile("GitLabOAuthAuthenticatorProviderTest-", "-empty");
emptyFile.deleteOnExit();
}
@Test(dataProvider = "noopConfig")
public void shouldProvideNoopAuthenticatorWhenInvalidConfigurationSet(
String gitHubClientIdPath, String gitHubClientSecretPath, String gitlabEndpoint)
throws IOException {
// given
GitLabOAuthAuthenticatorProvider provider =
new GitLabOAuthAuthenticatorProvider(
gitHubClientIdPath, gitHubClientSecretPath, gitlabEndpoint, "che.api");
// when
OAuthAuthenticator authenticator = provider.get();
// then
assertNotNull(authenticator);
assertTrue(
GitLabOAuthAuthenticatorProvider.NoopOAuthAuthenticator.class.isAssignableFrom(
authenticator.getClass()));
}
@Test
public void shouldProvideNoopAuthenticatorWhenConfigFilesAreEmpty() throws IOException {
// given
GitLabOAuthAuthenticatorProvider provider =
new GitLabOAuthAuthenticatorProvider(
emptyFile.getPath(), emptyFile.getPath(), TEST_URI, "che.api");
// when
OAuthAuthenticator authenticator = provider.get();
// then
assertNotNull(authenticator);
assertTrue(
GitLabOAuthAuthenticatorProvider.NoopOAuthAuthenticator.class.isAssignableFrom(
authenticator.getClass()));
}
@Test
public void shouldProvideValidGitLabOAuthAuthenticator() throws IOException {
// given
GitLabOAuthAuthenticatorProvider provider =
new GitLabOAuthAuthenticatorProvider(
credentialFile.getPath(), credentialFile.getPath(), TEST_URI, "che.api");
// when
OAuthAuthenticator authenticator = provider.get();
// then
assertNotNull(authenticator);
assertTrue(GitLabOAuthAuthenticator.class.isAssignableFrom(authenticator.getClass()));
}
@DataProvider(name = "noopConfig")
public Object[][] noopConfig() {
return new Object[][] {
{null, null, null},
{null, null, TEST_URI},
{"", "", TEST_URI},
{"", emptyFile.getPath(), TEST_URI},
{emptyFile.getPath(), "", TEST_URI},
{emptyFile.getPath(), emptyFile.getPath(), null},
{credentialFile.getPath(), credentialFile.getPath(), null},
{null, emptyFile.getPath(), TEST_URI},
{emptyFile.getPath(), null, TEST_URI},
{credentialFile.getPath(), null, TEST_URI},
{null, credentialFile.getPath(), TEST_URI},
{credentialFile.getPath(), "", TEST_URI},
{"", credentialFile.getPath(), TEST_URI},
{credentialFile.getPath(), null, null},
{credentialFile.getPath(), credentialFile.getPath(), ""},
{credentialFile.getPath(), credentialFile.getPath(), null},
{credentialFile.getPath(), emptyFile.getPath(), TEST_URI},
{emptyFile.getPath(), credentialFile.getPath(), TEST_URI},
};
}
}

View File

@ -14,6 +14,7 @@ package org.eclipse.che.api.factory.server.gitlab;
import static java.lang.String.format;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
@ -179,6 +180,8 @@ public class GitlabOAuthTokenFetcher implements PersonalAccessTokenFetcher {
return apiEndpoint
+ "/oauth/authenticate?oauth_provider="
+ OAUTH_PROVIDER_NAME
+ "&scope="
+ Joiner.on('+').join(DEFAULT_TOKEN_SCOPES)
+ "&request_method=POST&signature_method=rsa";
}

View File

@ -28,6 +28,7 @@
<module>che-core-api-auth</module>
<module>che-core-api-auth-bitbucket</module>
<module>che-core-api-auth-github</module>
<module>che-core-api-auth-gitlab</module>
<module>che-core-api-auth-openshift</module>
<module>che-core-api-workspace-shared</module>
<module>che-core-api-workspace</module>