diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml
index 2505c40d22..81ed9936ee 100644
--- a/assembly/assembly-wsmaster-war/pom.xml
+++ b/assembly/assembly-wsmaster-war/pom.xml
@@ -103,6 +103,10 @@
org.eclipse.che.core
che-core-api-auth-bitbucket
+
+ org.eclipse.che.core
+ che-core-api-auth-gitlab
+
org.eclipse.che.core
che-core-api-auth-openshift
diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java
index 19cb76512d..452918b8c7 100644
--- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java
+++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java
@@ -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);
diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties
index 5de83b82ad..854e1a315f 100644
--- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties
+++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties
@@ -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
diff --git a/pom.xml b/pom.xml
index 7c7bf44958..1124955d63 100644
--- a/pom.xml
+++ b/pom.xml
@@ -661,6 +661,11 @@
che-core-api-auth-github
${che.version}
+
+ org.eclipse.che.core
+ che-core-api-auth-gitlab
+ ${che.version}
+
org.eclipse.che.core
che-core-api-auth-openshift
diff --git a/wsmaster/che-core-api-auth-gitlab/pom.xml b/wsmaster/che-core-api-auth-gitlab/pom.xml
new file mode 100644
index 0000000000..15ce911dbd
--- /dev/null
+++ b/wsmaster/che-core-api-auth-gitlab/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+ 4.0.0
+
+ che-master-parent
+ org.eclipse.che.core
+ 7.42.0-SNAPSHOT
+
+ che-core-api-auth-gitlab
+ jar
+ Che Core :: API :: Authentication GitLab
+
+
+ com.google.guava
+ guava
+
+
+ com.google.http-client
+ google-http-client
+
+
+ com.google.inject
+ guice
+
+
+ com.sun.mail
+ jakarta.mail
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ org.eclipse.che.core
+ che-core-api-auth
+
+
+ org.eclipse.che.core
+ che-core-api-auth-shared
+
+
+ org.eclipse.che.core
+ che-core-commons-annotations
+
+
+ org.eclipse.che.core
+ che-core-commons-json
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.testng
+ testng
+ test
+
+
+
diff --git a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabModule.java b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabModule.java
new file mode 100644
index 0000000000..ca6d934fb0
--- /dev/null
+++ b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabModule.java
@@ -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 oAuthAuthenticators =
+ Multibinder.newSetBinder(binder(), OAuthAuthenticator.class);
+ oAuthAuthenticators.addBinding().toProvider(GitLabOAuthAuthenticatorProvider.class);
+ }
+}
diff --git a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java
new file mode 100644
index 0000000000..0967893733
--- /dev/null
+++ b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticator.java
@@ -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 getJson(String getUserUrl, String accessToken, Class userClass)
+ throws OAuthAuthenticationException {
+ HttpClient client = HttpClient.newHttpClient();
+ HttpRequest request =
+ HttpRequest.newBuilder(URI.create(getUserUrl))
+ .header("Authorization", "Bearer " + accessToken)
+ .build();
+
+ try {
+ HttpResponse 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;
+ }
+}
diff --git a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProvider.java b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProvider.java
new file mode 100644
index 0000000000..62c493aef3
--- /dev/null
+++ b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProvider.java
@@ -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 {
+ 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";
+ }
+ }
+}
diff --git a/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java
new file mode 100644
index 0000000000..2e463bef43
--- /dev/null
+++ b/wsmaster/che-core-api-auth-gitlab/src/main/java/org/eclipse/che/security/oauth/GitLabUser.java
@@ -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
+ + '\''
+ + '}';
+ }
+}
diff --git a/wsmaster/che-core-api-auth-gitlab/src/test/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProviderTest.java b/wsmaster/che-core-api-auth-gitlab/src/test/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProviderTest.java
new file mode 100644
index 0000000000..af846056a2
--- /dev/null
+++ b/wsmaster/che-core-api-auth-gitlab/src/test/java/org/eclipse/che/security/oauth/GitLabOAuthAuthenticatorProviderTest.java
@@ -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},
+ };
+ }
+}
diff --git a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcher.java b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcher.java
index f27d6eb8ef..f6d7aa0282 100644
--- a/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcher.java
+++ b/wsmaster/che-core-api-factory-gitlab/src/main/java/org/eclipse/che/api/factory/server/gitlab/GitlabOAuthTokenFetcher.java
@@ -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";
}
diff --git a/wsmaster/pom.xml b/wsmaster/pom.xml
index c41321885e..85a8f4ebc1 100644
--- a/wsmaster/pom.xml
+++ b/wsmaster/pom.xml
@@ -28,6 +28,7 @@
che-core-api-auth
che-core-api-auth-bitbucket
che-core-api-auth-github
+ che-core-api-auth-gitlab
che-core-api-auth-openshift
che-core-api-workspace-shared
che-core-api-workspace