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 47f9ada7321..5de83b82ad0 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 @@ -194,14 +194,13 @@ che.auth.access_denied_error_page=/error-oauth # Reserved user names che.auth.reserved_user_names= -# Configuration of GitHub OAuth client. -# You can setup GitHub OAuth to automate authentication to remote repositories. -# You need to first register this application with GitHub OAuth. -# GitHub OAuth client ID. -che.oauth.github.clientid=NULL - -# GitHub OAuth client secret. -che.oauth.github.clientsecret=NULL + +# Configuration of GitHub OAuth2 client. Used to obtain Personal access tokens. +# Location of the file with GitHub client id. +che.oauth2.github.clientid_filepath=NULL + +# Location of the file with GitHub client secret. +che.oauth2.github.clientsecret_filepath=NULL # GitHub OAuth authorization URI. che.oauth.github.authuri= https://github.com/login/oauth/authorize diff --git a/wsmaster/che-core-api-auth-github/pom.xml b/wsmaster/che-core-api-auth-github/pom.xml index e0a52f87a77..96423ffbe95 100644 --- a/wsmaster/che-core-api-auth-github/pom.xml +++ b/wsmaster/che-core-api-auth-github/pom.xml @@ -60,8 +60,13 @@ che-core-commons-inject - org.eclipse.che.core - che-core-commons-json + org.slf4j + slf4j-api + + + org.testng + testng + test diff --git a/wsmaster/che-core-api-auth-github/src/main/java/org/eclipse/che/security/oauth/GitHubOAuthAuthenticator.java b/wsmaster/che-core-api-auth-github/src/main/java/org/eclipse/che/security/oauth/GitHubOAuthAuthenticator.java index 6a9f319cbba..8f7354846b8 100644 --- a/wsmaster/che-core-api-auth-github/src/main/java/org/eclipse/che/security/oauth/GitHubOAuthAuthenticator.java +++ b/wsmaster/che-core-api-auth-github/src/main/java/org/eclipse/che/security/oauth/GitHubOAuthAuthenticator.java @@ -17,70 +17,31 @@ import jakarta.mail.internet.AddressException; import jakarta.mail.internet.InternetAddress; import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Type; -import java.net.HttpURLConnection; -import java.net.URL; -import javax.inject.Inject; -import javax.inject.Named; import javax.inject.Singleton; import org.eclipse.che.api.auth.shared.dto.OAuthToken; -import org.eclipse.che.commons.annotation.Nullable; -import org.eclipse.che.commons.json.JsonHelper; -import org.eclipse.che.commons.json.JsonParseException; import org.eclipse.che.security.oauth.shared.User; /** OAuth authentication for github account. */ @Singleton public class GitHubOAuthAuthenticator extends OAuthAuthenticator { - @Inject public GitHubOAuthAuthenticator( - @Nullable @Named("che.oauth.github.clientid") String clientId, - @Nullable @Named("che.oauth.github.clientsecret") String clientSecret, - @Nullable @Named("che.oauth.github.redirecturis") String[] redirectUris, - @Nullable @Named("che.oauth.github.authuri") String authUri, - @Nullable @Named("che.oauth.github.tokenuri") String tokenUri) + String clientId, String clientSecret, String[] redirectUris, String authUri, String tokenUri) throws IOException { - if (!isNullOrEmpty(clientId) - && !isNullOrEmpty(clientSecret) - && !isNullOrEmpty(authUri) - && !isNullOrEmpty(tokenUri) - && redirectUris != null - && redirectUris.length != 0) { - - configure( - clientId, clientSecret, redirectUris, authUri, tokenUri, new MemoryDataStoreFactory()); - } + configure( + clientId, clientSecret, redirectUris, authUri, tokenUri, new MemoryDataStoreFactory()); } @Override public User getUser(OAuthToken accessToken) throws OAuthAuthenticationException { GitHubUser user = - getJson( - "https://api.github.com/user?access_token=" + accessToken.getToken(), GitHubUser.class); - - GithubEmail[] result = - getJson2( - "https://api.github.com/user/emails?access_token=" + accessToken.getToken(), - GithubEmail[].class, - null); + getJson("https://api.github.com/user", accessToken.getToken(), GitHubUser.class); + final String email = user.getEmail(); - GithubEmail verifiedEmail = null; - for (GithubEmail email : result) { - if (email.isPrimary() && email.isVerified()) { - verifiedEmail = email; - break; - } - } - if (verifiedEmail == null - || verifiedEmail.getEmail() == null - || verifiedEmail.getEmail().isEmpty()) { + if (isNullOrEmpty(email)) { throw new OAuthAuthenticationException( "Sorry, we failed to find any verified emails associated with your GitHub account." + " Please, verify at least one email in your GitHub account and try to connect with GitHub again."); } - user.setEmail(verifiedEmail.getEmail()); - final String email = user.getEmail(); try { new InternetAddress(email).validate(); } catch (AddressException e) { @@ -89,32 +50,6 @@ public User getUser(OAuthToken accessToken) throws OAuthAuthenticationException return user; } - protected O getJson2(String getUserUrl, Class userClass, Type type) - throws OAuthAuthenticationException { - HttpURLConnection urlConnection = null; - InputStream urlInputStream = null; - - try { - urlConnection = (HttpURLConnection) new URL(getUserUrl).openConnection(); - urlConnection.setRequestProperty("Accept", "application/vnd.github.v3.html+json"); - urlInputStream = urlConnection.getInputStream(); - return JsonHelper.fromJson(urlInputStream, userClass, type); - } catch (JsonParseException | IOException e) { - throw new OAuthAuthenticationException(e.getMessage(), e); - } finally { - if (urlInputStream != null) { - try { - urlInputStream.close(); - } catch (IOException ignored) { - } - } - - if (urlConnection != null) { - urlConnection.disconnect(); - } - } - } - @Override public final String getOAuthProvider() { return "github"; @@ -123,62 +58,19 @@ public final String getOAuthProvider() { @Override public OAuthToken getToken(String userId) throws IOException { final OAuthToken token = super.getToken(userId); - if (!(token == null || token.getToken() == null || token.getToken().isEmpty())) { - // Need to check if token which stored is valid for requests, then if valid - we returns it to - // caller - String tokenVerifyUrl = "https://api.github.com/?access_token=" + token.getToken(); - HttpURLConnection http = null; - try { - http = (HttpURLConnection) new URL(tokenVerifyUrl).openConnection(); - http.setInstanceFollowRedirects(false); - http.setRequestMethod("GET"); - http.setRequestProperty("Accept", "application/json"); - - if (http.getResponseCode() == 401) { - return null; - } - } finally { - if (http != null) { - http.disconnect(); - } + // Need to check if token which is stored is valid for requests, then if valid - we returns it + // to + // caller + try { + if (token == null + || token.getToken() == null + || token.getToken().isEmpty() + || getJson("https://api.github.com/user", token.getToken(), GitHubUser.class) == null) { + return null; } - - return token; - } - return null; - } - - /** - * information for each email address indicating if the address has been verified and if it’s the - * user’s primary email address for GitHub. - */ - public static class GithubEmail { - private boolean primary; - private boolean verified; - private String email; - - public boolean isPrimary() { - return primary; - } - - public void setPrimary(boolean primary) { - this.primary = primary; - } - - public boolean isVerified() { - return verified; - } - - public void setVerified(boolean verified) { - this.verified = verified; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; + } catch (OAuthAuthenticationException e) { + return null; } + return token; } } diff --git a/wsmaster/che-core-api-auth-github/src/main/java/org/eclipse/che/security/oauth/GitHubOAuthAuthenticatorProvider.java b/wsmaster/che-core-api-auth-github/src/main/java/org/eclipse/che/security/oauth/GitHubOAuthAuthenticatorProvider.java new file mode 100644 index 00000000000..4a101737eee --- /dev/null +++ b/wsmaster/che-core-api-auth-github/src/main/java/org/eclipse/che/security/oauth/GitHubOAuthAuthenticatorProvider.java @@ -0,0 +1,95 @@ +/* + * 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 java.util.Objects; +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 GitHub {@link OAuthAuthenticator} based on available configuration. + * + * @author Pavol Baran + */ +@Singleton +public class GitHubOAuthAuthenticatorProvider implements Provider { + private static final Logger LOG = LoggerFactory.getLogger(GitHubOAuthAuthenticatorProvider.class); + private final OAuthAuthenticator authenticator; + + @Inject + public GitHubOAuthAuthenticatorProvider( + @Nullable @Named("che.oauth2.github.clientid_filepath") String gitHubClientIdPath, + @Nullable @Named("che.oauth2.github.clientsecret_filepath") String gitHubClientSecretPath, + @Nullable @Named("che.oauth.github.redirecturis") String[] redirectUris, + @Nullable @Named("che.oauth.github.authuri") String authUri, + @Nullable @Named("che.oauth.github.tokenuri") String tokenUri) + throws IOException { + authenticator = + getOAuthAuthenticator( + gitHubClientIdPath, gitHubClientSecretPath, redirectUris, authUri, tokenUri); + LOG.debug("{} GitHub OAuth Authenticator is used.", authenticator); + } + + @Override + public OAuthAuthenticator get() { + return authenticator; + } + + private OAuthAuthenticator getOAuthAuthenticator( + String clientIdPath, + String clientSecretPath, + String[] redirectUris, + String authUri, + String tokenUri) + throws IOException { + + if (!isNullOrEmpty(clientIdPath) + && !isNullOrEmpty(clientSecretPath) + && !isNullOrEmpty(authUri) + && !isNullOrEmpty(tokenUri) + && Objects.nonNull(redirectUris) + && redirectUris.length != 0) { + String clientId = Files.readString(Path.of(clientIdPath)); + String clientSecret = Files.readString(Path.of(clientSecretPath)); + if (!isNullOrEmpty(clientId) && !isNullOrEmpty(clientSecret)) { + return new GitHubOAuthAuthenticator( + clientId, clientSecret, redirectUris, authUri, tokenUri); + } + } + 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 GitHub authentication. Make sure OAuth is properly configured."); + } + + @Override + public String getOAuthProvider() { + return "Noop"; + } + } +} diff --git a/wsmaster/che-core-api-auth-github/src/main/java/org/eclipse/che/security/oauth/GithubModule.java b/wsmaster/che-core-api-auth-github/src/main/java/org/eclipse/che/security/oauth/GithubModule.java index 788594ad373..16ef10f5e2e 100644 --- a/wsmaster/che-core-api-auth-github/src/main/java/org/eclipse/che/security/oauth/GithubModule.java +++ b/wsmaster/che-core-api-auth-github/src/main/java/org/eclipse/che/security/oauth/GithubModule.java @@ -26,6 +26,6 @@ public class GithubModule extends AbstractModule { protected void configure() { Multibinder oAuthAuthenticators = Multibinder.newSetBinder(binder(), OAuthAuthenticator.class); - oAuthAuthenticators.addBinding().to(GitHubOAuthAuthenticator.class); + oAuthAuthenticators.addBinding().toProvider(GitHubOAuthAuthenticatorProvider.class); } } diff --git a/wsmaster/che-core-api-auth-github/src/test/java/org/eclipse/che/security/oauth/GitHubOAuthAuthenticatorProviderTest.java b/wsmaster/che-core-api-auth-github/src/test/java/org/eclipse/che/security/oauth/GitHubOAuthAuthenticatorProviderTest.java new file mode 100644 index 00000000000..fe42ff67c6a --- /dev/null +++ b/wsmaster/che-core-api-auth-github/src/test/java/org/eclipse/che/security/oauth/GitHubOAuthAuthenticatorProviderTest.java @@ -0,0 +1,114 @@ +/* + * 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 GitHubOAuthAuthenticatorProviderTest { + private static final String TEST_URI = "https://api.github.com"; + private File credentialFile; + private File emptyFile; + + @BeforeClass + public void setup() throws IOException { + credentialFile = File.createTempFile("GitHubOAuthAuthenticatorProviderTest-", "-credentials"); + Files.asCharSink(credentialFile, Charset.defaultCharset()).write("id/secret"); + credentialFile.deleteOnExit(); + emptyFile = File.createTempFile("GitHubOAuthAuthenticatorProviderTest-", "-empty"); + emptyFile.deleteOnExit(); + } + + @Test(dataProvider = "noopConfig") + public void shouldProvideNoopAuthenticatorWhenInvalidConfigurationSet( + String gitHubClientIdPath, + String gitHubClientSecretPath, + String[] redirectUris, + String authUri, + String tokenUri) + throws IOException { + // given + GitHubOAuthAuthenticatorProvider provider = + new GitHubOAuthAuthenticatorProvider( + gitHubClientIdPath, gitHubClientSecretPath, redirectUris, authUri, tokenUri); + // when + OAuthAuthenticator authenticator = provider.get(); + // then + assertNotNull(authenticator); + assertTrue( + GitHubOAuthAuthenticatorProvider.NoopOAuthAuthenticator.class.isAssignableFrom( + authenticator.getClass())); + } + + @Test + public void shouldProvideNoopAuthenticatorWhenConfigFilesAreEmpty() throws IOException { + // given + GitHubOAuthAuthenticatorProvider provider = + new GitHubOAuthAuthenticatorProvider( + emptyFile.getPath(), emptyFile.getPath(), new String[] {TEST_URI}, TEST_URI, TEST_URI); + // when + OAuthAuthenticator authenticator = provider.get(); + // then + assertNotNull(authenticator); + assertTrue( + GitHubOAuthAuthenticatorProvider.NoopOAuthAuthenticator.class.isAssignableFrom( + authenticator.getClass())); + } + + @Test + public void shouldProvideValidGitHubOAuthAuthenticator() throws IOException { + // given + GitHubOAuthAuthenticatorProvider provider = + new GitHubOAuthAuthenticatorProvider( + credentialFile.getPath(), + credentialFile.getPath(), + new String[] {TEST_URI}, + TEST_URI, + TEST_URI); + // when + OAuthAuthenticator authenticator = provider.get(); + + // then + assertNotNull(authenticator); + assertTrue(GitHubOAuthAuthenticator.class.isAssignableFrom(authenticator.getClass())); + } + + @DataProvider(name = "noopConfig") + public Object[][] noopConfig() { + return new Object[][] { + {null, null, null, null, null}, + {credentialFile.getPath(), emptyFile.getPath(), null, TEST_URI, null}, + {emptyFile.getPath(), emptyFile.getPath(), null, null, TEST_URI}, + {null, emptyFile.getPath(), null, TEST_URI, TEST_URI}, + {null, credentialFile.getPath(), new String[] {}, null, null}, + {emptyFile.getPath(), null, new String[] {}, "", ""}, + {credentialFile.getPath(), null, new String[] {}, "", null}, + {null, emptyFile.getPath(), new String[] {}, null, ""}, + {credentialFile.getPath(), null, new String[] {}, TEST_URI, null}, + {null, emptyFile.getPath(), new String[] {}, TEST_URI, TEST_URI}, + {emptyFile.getPath(), null, new String[] {TEST_URI}, null, null}, + {credentialFile.getPath(), null, new String[] {TEST_URI}, "", ""}, + {credentialFile.getPath(), credentialFile.getPath(), new String[] {TEST_URI}, null, TEST_URI}, + {credentialFile.getPath(), emptyFile.getPath(), new String[] {TEST_URI}, TEST_URI, null}, + {credentialFile.getPath(), credentialFile.getPath(), new String[] {TEST_URI}, TEST_URI, ""}, + {emptyFile.getPath(), emptyFile.getPath(), new String[] {TEST_URI}, "", TEST_URI} + }; + } +} diff --git a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticator.java b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticator.java index a0703c431d3..61c6bb5b90f 100644 --- a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticator.java +++ b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticator.java @@ -38,6 +38,7 @@ import java.util.Map; import java.util.regex.Pattern; import org.eclipse.che.api.auth.shared.dto.OAuthToken; +import org.eclipse.che.commons.env.EnvironmentContext; import org.eclipse.che.commons.json.JsonHelper; import org.eclipse.che.commons.json.JsonParseException; import org.eclipse.che.security.oauth.shared.OAuthTokenProvider; @@ -213,8 +214,7 @@ public String callback(URL requestUrl, List scopes) throws OAuthAuthenti .execute(); String userId = getUserFromUrl(authorizationCodeResponseUrl); if (userId == null) { - userId = - getUser(newDto(OAuthToken.class).withToken(tokenResponse.getAccessToken())).getId(); + userId = EnvironmentContext.getCurrent().getSubject().getUserId(); } flow.createAndStoreCredential(tokenResponse, userId); return userId; @@ -254,13 +254,14 @@ private String getUserFromUrl(AuthorizationCodeResponseUrl authorizationCodeResp return null; } - protected O getJson(String getUserUrl, Class userClass) + protected O getJson(String getUserUrl, String accessToken, Class userClass) throws OAuthAuthenticationException { HttpURLConnection urlConnection = null; InputStream urlInputStream = null; try { urlConnection = (HttpURLConnection) new URL(getUserUrl).openConnection(); + urlConnection.setRequestProperty("Authorization", "token " + accessToken); urlInputStream = urlConnection.getInputStream(); return JsonHelper.fromJson(urlInputStream, userClass, null); } catch (JsonParseException | IOException e) { diff --git a/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcher.java b/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcher.java index 730040d7ec1..daf7a32ee55 100644 --- a/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcher.java +++ b/wsmaster/che-core-api-factory-github/src/main/java/org/eclipse/che/api/factory/server/github/GithubPersonalAccessTokenFetcher.java @@ -11,6 +11,7 @@ */ package org.eclipse.che.api.factory.server.github; +import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.util.Arrays; @@ -250,6 +251,8 @@ private String getLocalAuthenticateUrl() { return apiEndpoint + "/oauth/authenticate?oauth_provider=" + OAUTH_PROVIDER_NAME + + "&scope=" + + Joiner.on(',').join(DEFAULT_TOKEN_SCOPES) + "&request_method=POST&signature_method=rsa"; } }