From cd99cf4e6c869da547d26f85ba7600127b65cd00 Mon Sep 17 00:00:00 2001 From: Mykhailo Kuznietsov Date: Mon, 14 May 2018 17:37:14 +0300 Subject: [PATCH] Add ability to configure OAuth type in Multi User Che (#9640) --- .../che/api/deploy/WsMasterModule.java | 5 +- .../WEB-INF/classes/che/multiuser.properties | 6 + .../che-multiuser-keycloak-server/pom.xml | 4 + .../server/deploy/KeycloakModule.java | 5 +- .../server/deploy/OAuthAPIProvider.java | 53 ++++++ ...ionService.java => DelegatedOAuthAPI.java} | 79 ++++---- .../che/security/oauth/EmbeddedOAuthAPI.java | 163 +++++++++++++++++ .../eclipse/che/security/oauth/OAuthAPI.java | 58 ++++++ .../oauth/OAuthAuthenticationService.java | 169 ++++-------------- .../security/oauth/EmbeddedOAuthAPITest.java | 57 ++++++ .../oauth/OAuthAuthenticationServiceTest.java | 110 ------------ 11 files changed, 421 insertions(+), 288 deletions(-) create mode 100644 multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/OAuthAPIProvider.java rename multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/oauth2/{KeycloakOAuthAuthenticationService.java => DelegatedOAuthAPI.java} (55%) create mode 100644 wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPI.java create mode 100644 wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAPI.java create mode 100644 wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPITest.java delete mode 100644 wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/OAuthAuthenticationServiceTest.java 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 330c8d695d..2dcd107dd4 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 @@ -86,6 +86,8 @@ import org.eclipse.che.multiuser.resource.api.ResourceModule; import org.eclipse.che.plugin.github.factory.resolver.GithubFactoryParametersResolver; 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.OAuthAPI; import org.eclipse.che.workspace.infrastructure.docker.DockerInfraModule; import org.eclipse.che.workspace.infrastructure.docker.local.LocalDockerModule; import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfraModule; @@ -137,6 +139,7 @@ public class WsMasterModule extends AbstractModule { bind(org.eclipse.che.api.user.server.UserService.class); bind(org.eclipse.che.api.user.server.ProfileService.class); bind(org.eclipse.che.api.user.server.PreferencesService.class); + bind(org.eclipse.che.security.oauth.OAuthAuthenticationService.class); MapBinder stacks = MapBinder.newMapBinder( @@ -278,7 +281,7 @@ public class WsMasterModule extends AbstractModule { bind(org.eclipse.che.security.oauth.shared.OAuthTokenProvider.class) .to(org.eclipse.che.security.oauth.OAuthAuthenticatorTokenProvider.class); - bind(org.eclipse.che.security.oauth.OAuthAuthenticationService.class); + bind(OAuthAPI.class).to(EmbeddedOAuthAPI.class); bind(RemoteSubscriptionStorage.class).to(InmemoryRemoteSubscriptionStorage.class); install(new org.eclipse.che.api.workspace.activity.inject.WorkspaceActivityModule()); diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties index 9cef031807..f2b4362fed 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/multiuser.properties @@ -136,3 +136,9 @@ che.keycloak.js_adapter_url=NULL # a discovery endpoint as detailed in the following specification # https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig che.keycloak.oidc_provider=NULL + +# Configuration of OAuth Authentication Service that can be used in "embedded" or "delegated" mode. +# If set to "embedded", then the service work as a wrapper to Che's OAuthAuthenticator ( as in Single User mode). +# If set to "delegated", then the service will use Keycloak IdentityProvider mechanism. +# Runtime Exception wii be thrown, in case if this property is not set properly. +che.oauth.service_mode=delegated diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml b/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml index 502952b6d5..49707faed7 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml +++ b/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml @@ -77,6 +77,10 @@ org.eclipse.che.core che-core-api-account + + org.eclipse.che.core + che-core-api-auth + org.eclipse.che.core che-core-api-auth-shared diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java index 9adc0596cf..399fe6c455 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java @@ -19,7 +19,7 @@ import org.eclipse.che.multiuser.keycloak.server.KeycloakConfigurationService; import org.eclipse.che.multiuser.keycloak.server.KeycloakTokenValidator; import org.eclipse.che.multiuser.keycloak.server.KeycloakUserManager; import org.eclipse.che.multiuser.keycloak.server.dao.KeycloakProfileDao; -import org.eclipse.che.multiuser.keycloak.server.oauth2.KeycloakOAuthAuthenticationService; +import org.eclipse.che.security.oauth.OAuthAPI; public class KeycloakModule extends AbstractModule { @Override @@ -29,9 +29,10 @@ public class KeycloakModule extends AbstractModule { .to(org.eclipse.che.multiuser.keycloak.server.KeycloakHttpJsonRequestFactory.class); bind(TokenValidator.class).to(KeycloakTokenValidator.class); bind(KeycloakConfigurationService.class); - bind(KeycloakOAuthAuthenticationService.class); bind(ProfileDao.class).to(KeycloakProfileDao.class); bind(PersonalAccountUserManager.class).to(KeycloakUserManager.class); + + bind(OAuthAPI.class).toProvider(OAuthAPIProvider.class); } } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/OAuthAPIProvider.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/OAuthAPIProvider.java new file mode 100644 index 0000000000..6d0f4e2357 --- /dev/null +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/OAuthAPIProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.multiuser.keycloak.server.deploy; + +import com.google.inject.Injector; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.multiuser.keycloak.server.oauth2.DelegatedOAuthAPI; +import org.eclipse.che.security.oauth.EmbeddedOAuthAPI; +import org.eclipse.che.security.oauth.OAuthAPI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides appropriate OAuth Authentication API depending on configuration. + * + * @author Mykhailo Kuznietsov. + */ +public class OAuthAPIProvider implements Provider { + private static final Logger LOG = LoggerFactory.getLogger(OAuthAPIProvider.class); + private String oauthType; + private Injector injector; + + @Inject + public OAuthAPIProvider( + @Nullable @Named("che.oauth.service_mode") String oauthType, Injector injector) { + this.oauthType = oauthType; + this.injector = injector; + } + + @Override + public OAuthAPI get() { + switch (oauthType) { + case "embedded": + return injector.getInstance(EmbeddedOAuthAPI.class); + case "delegated": + return injector.getInstance(DelegatedOAuthAPI.class); + default: + throw new RuntimeException( + "Unknown value configured for \"che.oauth.service_mode\", must be either \"embedded\", or \"delegated\""); + } + } +} diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/oauth2/KeycloakOAuthAuthenticationService.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/oauth2/DelegatedOAuthAPI.java similarity index 55% rename from multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/oauth2/KeycloakOAuthAuthenticationService.java rename to multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/oauth2/DelegatedOAuthAPI.java index 171fdedbe8..1dfcfb0d22 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/oauth2/KeycloakOAuthAuthenticationService.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/oauth2/DelegatedOAuthAPI.java @@ -13,54 +13,48 @@ package org.eclipse.che.multiuser.keycloak.server.oauth2; import io.jsonwebtoken.Jwt; import java.io.IOException; import java.net.URI; +import java.util.List; +import java.util.Set; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriInfo; import org.eclipse.che.api.auth.shared.dto.OAuthToken; import org.eclipse.che.api.core.BadRequestException; -import org.eclipse.che.api.core.ConflictException; import org.eclipse.che.api.core.ForbiddenException; import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.UnauthorizedException; -import org.eclipse.che.api.core.rest.annotations.Required; import org.eclipse.che.dto.server.DtoFactory; import org.eclipse.che.multiuser.keycloak.server.KeycloakServiceClient; import org.eclipse.che.multiuser.keycloak.shared.dto.KeycloakTokenResponse; +import org.eclipse.che.security.oauth.OAuthAPI; +import org.eclipse.che.security.oauth.OAuthAuthenticationService; +import org.eclipse.che.security.oauth.shared.dto.OAuthAuthenticatorDescriptor; -@Path("/oauth") -public class KeycloakOAuthAuthenticationService { - @Context UriInfo uriInfo; - - @Context SecurityContext security; +/** + * Implementation of functional API component for {@link OAuthAuthenticationService}, that uses + * {@link KeycloakServiceClient} for authenticating users through Keycloak Identity providers. + * + * @author Mykhailo Kuznietsov + */ +public class DelegatedOAuthAPI implements OAuthAPI { private final KeycloakServiceClient keycloakServiceClient; @Inject - public KeycloakOAuthAuthenticationService(KeycloakServiceClient keycloakServiceClient) { + public DelegatedOAuthAPI(KeycloakServiceClient keycloakServiceClient) { this.keycloakServiceClient = keycloakServiceClient; } - /** - * Performs local and Keycloak accounts linking - * - * @return typically Response that redirect user for OAuth provider site - */ - @GET - @Path("authenticate") + @Override public Response authenticate( - @Required @QueryParam("oauth_provider") String oauthProvider, - @Required @QueryParam("redirect_after_login") String redirectAfterLogin, - @Context HttpServletRequest request) - throws ForbiddenException, BadRequestException { + UriInfo uriInfo, + String oauthProvider, + List scopes, + String redirectAfterLogin, + HttpServletRequest request) + throws BadRequestException { Jwt jwtToken = (Jwt) request.getAttribute("token"); if (jwtToken == null) { @@ -71,19 +65,10 @@ public class KeycloakOAuthAuthenticationService { return Response.temporaryRedirect(URI.create(accountLinkUrl)).build(); } - /** - * Gets OAuth token for user from Keycloak. - * - * @param oauthProvider OAuth provider name - * @return OAuthToken - * @throws ServerException - */ - @GET - @Path("token") - @Produces(MediaType.APPLICATION_JSON) - public OAuthToken token(@Required @QueryParam("oauth_provider") String oauthProvider) - throws ForbiddenException, BadRequestException, ConflictException, NotFoundException, - ServerException, UnauthorizedException { + @Override + public OAuthToken getToken(String oauthProvider) + throws ForbiddenException, BadRequestException, NotFoundException, ServerException, + UnauthorizedException { try { KeycloakTokenResponse response = keycloakServiceClient.getIdentityProviderToken(oauthProvider); @@ -94,4 +79,20 @@ public class KeycloakOAuthAuthenticationService { throw new ServerException(e.getMessage()); } } + + @Override + public void invalidateToken(String oauthProvider) throws ForbiddenException { + throw new ForbiddenException("Method is not supported in this implementation of OAuth API"); + } + + @Override + public Response callback(UriInfo uriInfo, List errorValues) throws ForbiddenException { + throw new ForbiddenException("Method is not supported in this implementation of OAuth API"); + } + + @Override + public Set getRegisteredAuthenticators(UriInfo uriInfo) + throws ForbiddenException { + throw new ForbiddenException("Method is not supported in this implementation of OAuth API"); + } } diff --git a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPI.java b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPI.java new file mode 100644 index 0000000000..0897957c3b --- /dev/null +++ b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPI.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.security.oauth; + +import static java.util.Collections.emptyList; +import static org.eclipse.che.commons.lang.UrlUtils.*; +import static org.eclipse.che.commons.lang.UrlUtils.getParameter; +import static org.eclipse.che.dto.server.DtoFactory.newDto; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.*; +import javax.inject.Inject; +import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import org.eclipse.che.api.auth.shared.dto.OAuthToken; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.UnauthorizedException; +import org.eclipse.che.api.core.rest.shared.dto.Link; +import org.eclipse.che.api.core.rest.shared.dto.LinkParameter; +import org.eclipse.che.api.core.util.LinksHelper; +import org.eclipse.che.commons.env.EnvironmentContext; +import org.eclipse.che.commons.subject.Subject; +import org.eclipse.che.security.oauth.shared.dto.OAuthAuthenticatorDescriptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of functional API component for {@link OAuthAuthenticationService}, that uses + * {@link OAuthAuthenticator}. + * + * @author Mykhailo Kuznietsov + */ +public class EmbeddedOAuthAPI implements OAuthAPI { + private static final Logger LOG = LoggerFactory.getLogger(EmbeddedOAuthAPI.class); + + @Inject + @Named("che.auth.access_denied_error_page") + protected String errorPage; + + @Inject protected OAuthAuthenticatorProvider providers; + + @Override + public Response authenticate( + UriInfo uriInfo, + String oauthProvider, + List scopes, + String redirectAfterLogin, + HttpServletRequest request) + throws NotFoundException, OAuthAuthenticationException { + OAuthAuthenticator oauth = getAuthenticator(oauthProvider); + final String authUrl = + oauth.getAuthenticateUrl(getRequestUrl(uriInfo), scopes == null ? emptyList() : scopes); + return Response.temporaryRedirect(URI.create(authUrl)).build(); + } + + @Override + public Response callback(UriInfo uriInfo, List errorValues) + throws NotFoundException, OAuthAuthenticationException { + URL requestUrl = getRequestUrl(uriInfo); + Map> params = getQueryParametersFromState(getState(requestUrl)); + if (errorValues != null && errorValues.contains("access_denied")) { + return Response.temporaryRedirect( + uriInfo.getRequestUriBuilder().replacePath(errorPage).replaceQuery(null).build()) + .build(); + } + final String providerName = getParameter(params, "oauth_provider"); + OAuthAuthenticator oauth = getAuthenticator(providerName); + final List scopes = params.get("scope"); + oauth.callback(requestUrl, scopes == null ? Collections.emptyList() : scopes); + final String redirectAfterLogin = getParameter(params, "redirect_after_login"); + return Response.temporaryRedirect(URI.create(redirectAfterLogin)).build(); + } + + @Override + public Set getRegisteredAuthenticators(UriInfo uriInfo) { + Set result = new HashSet<>(); + final UriBuilder uriBuilder = + uriInfo.getBaseUriBuilder().clone().path(OAuthAuthenticationService.class); + for (String name : providers.getRegisteredProviderNames()) { + final List links = new LinkedList<>(); + links.add( + LinksHelper.createLink( + HttpMethod.GET, + uriBuilder + .clone() + .path(OAuthAuthenticationService.class, "authenticate") + .build() + .toString(), + null, + null, + "Authenticate URL", + newDto(LinkParameter.class) + .withName("oauth_provider") + .withRequired(true) + .withDefaultValue(name), + newDto(LinkParameter.class) + .withName("mode") + .withRequired(true) + .withDefaultValue("federated_login"))); + result.add(newDto(OAuthAuthenticatorDescriptor.class).withName(name).withLinks(links)); + } + return result; + } + + @Override + public OAuthToken getToken(String oauthProvider) + throws NotFoundException, UnauthorizedException, ServerException { + OAuthAuthenticator provider = getAuthenticator(oauthProvider); + final Subject subject = EnvironmentContext.getCurrent().getSubject(); + try { + OAuthToken token = provider.getToken(subject.getUserId()); + if (token == null) { + token = provider.getToken(subject.getUserName()); + } + if (token != null) { + return token; + } + throw new UnauthorizedException( + "OAuth token for user " + subject.getUserId() + " was not found"); + } catch (IOException e) { + throw new ServerException(e.getLocalizedMessage(), e); + } + } + + @Override + public void invalidateToken(String oauthProvider) + throws NotFoundException, UnauthorizedException, ServerException { + OAuthAuthenticator oauth = getAuthenticator(oauthProvider); + final Subject subject = EnvironmentContext.getCurrent().getSubject(); + try { + if (!oauth.invalidateToken(subject.getUserId())) { + throw new UnauthorizedException( + "OAuth token for user " + subject.getUserId() + " was not found"); + } + } catch (IOException e) { + throw new ServerException(e.getMessage()); + } + } + + protected OAuthAuthenticator getAuthenticator(String oauthProviderName) throws NotFoundException { + OAuthAuthenticator oauth = providers.getAuthenticator(oauthProviderName); + if (oauth == null) { + LOG.warn("Unsupported OAuth provider {} ", oauthProviderName); + throw new NotFoundException("Unsupported OAuth provider " + oauthProviderName); + } + return oauth; + } +} diff --git a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAPI.java b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAPI.java new file mode 100644 index 0000000000..ee238d495e --- /dev/null +++ b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAPI.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.security.oauth; + +import java.util.List; +import java.util.Set; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import org.eclipse.che.api.auth.shared.dto.OAuthToken; +import org.eclipse.che.api.core.*; +import org.eclipse.che.security.oauth.shared.dto.OAuthAuthenticatorDescriptor; + +/** + * Interface of OAuth authentication service API component, that is used for. + * + * @author Mykhailo Kuznietsov + */ +public interface OAuthAPI { + + /** + * Implementation of method {@link OAuthAuthenticationService#authenticate(String, String, List, + * HttpServletRequest)} + */ + Response authenticate( + UriInfo uriInfo, + String oauthProvider, + List scopes, + String redirectAfterLogin, + HttpServletRequest request) + throws NotFoundException, OAuthAuthenticationException, ForbiddenException, + BadRequestException; + + /** Implementation of method {@link OAuthAuthenticationService#callback(List)} */ + Response callback(UriInfo uriInfo, List errorValues) + throws NotFoundException, OAuthAuthenticationException, ForbiddenException; + + /** Implementation of method {@link OAuthAuthenticationService#getRegisteredAuthenticators()} */ + Set getRegisteredAuthenticators(UriInfo uriInfo) + throws ForbiddenException; + + /** Implementation of method {@link OAuthAuthenticationService#token(String)} */ + OAuthToken getToken(String oauthProvider) + throws NotFoundException, UnauthorizedException, ServerException, ForbiddenException, + BadRequestException, ConflictException; + + /** Implementation of method {@link OAuthAuthenticationService#invalidate(String)}} */ + void invalidateToken(String oauthProvider) + throws NotFoundException, UnauthorizedException, ServerException, ForbiddenException; +} diff --git a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticationService.java b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticationService.java index 5b793a6ac7..9835c205a4 100644 --- a/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticationService.java +++ b/wsmaster/che-core-api-auth/src/main/java/org/eclipse/che/security/oauth/OAuthAuthenticationService.java @@ -10,27 +10,12 @@ */ package org.eclipse.che.security.oauth; -import static java.util.Collections.emptyList; -import static org.eclipse.che.commons.lang.UrlUtils.getParameter; -import static org.eclipse.che.commons.lang.UrlUtils.getQueryParametersFromState; -import static org.eclipse.che.commons.lang.UrlUtils.getRequestUrl; -import static org.eclipse.che.commons.lang.UrlUtils.getState; -import static org.eclipse.che.dto.server.DtoFactory.newDto; - -import java.io.IOException; -import java.net.URI; -import java.net.URL; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Set; import javax.inject.Inject; -import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.DELETE; import javax.ws.rs.GET; -import javax.ws.rs.HttpMethod; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; @@ -38,89 +23,49 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import org.eclipse.che.api.auth.shared.dto.OAuthToken; -import org.eclipse.che.api.core.ForbiddenException; -import org.eclipse.che.api.core.NotFoundException; -import org.eclipse.che.api.core.ServerException; -import org.eclipse.che.api.core.UnauthorizedException; +import org.eclipse.che.api.core.*; +import org.eclipse.che.api.core.rest.Service; import org.eclipse.che.api.core.rest.annotations.Required; -import org.eclipse.che.api.core.rest.shared.dto.Link; -import org.eclipse.che.api.core.rest.shared.dto.LinkParameter; -import org.eclipse.che.api.core.util.LinksHelper; -import org.eclipse.che.commons.env.EnvironmentContext; -import org.eclipse.che.commons.subject.Subject; import org.eclipse.che.security.oauth.shared.dto.OAuthAuthenticatorDescriptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** RESTful wrapper for OAuthAuthenticator. */ @Path("oauth") -public class OAuthAuthenticationService { - private static final Logger LOG = LoggerFactory.getLogger(OAuthAuthenticationService.class); - - @Inject - @Named("che.auth.access_denied_error_page") - protected String errorPage; - - @Inject protected OAuthAuthenticatorProvider providers; +public class OAuthAuthenticationService extends Service { @Context protected UriInfo uriInfo; @Context protected SecurityContext security; + @Inject private OAuthAPI oAuthAPI; + /** - * Redirect request to OAuth provider site for authentication|authorization. Client request must - * contains set of required query parameters: - * - * - * - * - * - * - * - *
NameDescriptionMandatoryDefault value
oauth_providerName of OAuth provider. At the moment google and github - * supportedyesnone
scopeSpecify exactly what type of access needed. List of scopes dependents to OAuth provider. - * Requested scopes displayed at user authorization page at OAuth provider site. Check docs about scopes - * supported by - * suitable OAuth provider.noEmpty list
modeAuthentication mode. May be federated_login or token. If mode - * set - * as federated_login that parameters 'username' and 'password' added to redirect URL after successful - * user - * authentication. (see next parameter) In this case 'password' is temporary generated password. This password will - * be validated by FederatedLoginModule.notoken
redirect_after_loginURL for user redirection after successful - * authenticationyesnone
+ * Redirect request to OAuth provider site for authentication|authorization. Client must provide + * query parameters, that may or may not be required, depending on the active implementation of + * {@link OAuthAPI}. * + * @param oauthProvider - + * @param redirectAfterLogin + * @param scopes - list * @return typically Response that redirect user for OAuth provider site */ @GET @Path("authenticate") public Response authenticate( - @Required @QueryParam("oauth_provider") String oauthProvider, - @QueryParam("scope") List scopes) - throws ForbiddenException, NotFoundException, OAuthAuthenticationException { - OAuthAuthenticator oauth = getAuthenticator(oauthProvider); - final String authUrl = - oauth.getAuthenticateUrl(getRequestUrl(uriInfo), scopes == null ? emptyList() : scopes); - return Response.temporaryRedirect(URI.create(authUrl)).build(); + @QueryParam("oauth_provider") String oauthProvider, + @QueryParam("redirect_after_login") String redirectAfterLogin, + @QueryParam("scope") List scopes, + @Context HttpServletRequest request) + throws NotFoundException, OAuthAuthenticationException, BadRequestException, + ForbiddenException { + return oAuthAPI.authenticate(uriInfo, oauthProvider, scopes, redirectAfterLogin, request); } @GET @Path("callback") + /** Process OAuth callback */ public Response callback(@QueryParam("errorValues") List errorValues) - throws OAuthAuthenticationException, NotFoundException { - URL requestUrl = getRequestUrl(uriInfo); - Map> params = getQueryParametersFromState(getState(requestUrl)); - if (errorValues != null && errorValues.contains("access_denied")) { - return Response.temporaryRedirect( - uriInfo.getRequestUriBuilder().replacePath(errorPage).replaceQuery(null).build()) - .build(); - } - final String providerName = getParameter(params, "oauth_provider"); - OAuthAuthenticator oauth = getAuthenticator(providerName); - final List scopes = params.get("scope"); - oauth.callback(requestUrl, scopes == null ? Collections.emptyList() : scopes); - final String redirectAfterLogin = getParameter(params, "redirect_after_login"); - return Response.temporaryRedirect(URI.create(redirectAfterLogin)).build(); + throws OAuthAuthenticationException, NotFoundException, ForbiddenException { + return oAuthAPI.callback(uriInfo, errorValues); } /** @@ -130,29 +75,8 @@ public class OAuthAuthenticationService { */ @GET @Produces(MediaType.APPLICATION_JSON) - public Set getRegisteredAuthenticators() { - Set result = new HashSet<>(); - final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder().clone().path(getClass()); - for (String name : providers.getRegisteredProviderNames()) { - final List links = new LinkedList<>(); - links.add( - LinksHelper.createLink( - HttpMethod.GET, - uriBuilder.clone().path(getClass(), "authenticate").build().toString(), - null, - null, - "Authenticate URL", - newDto(LinkParameter.class) - .withName("oauth_provider") - .withRequired(true) - .withDefaultValue(name), - newDto(LinkParameter.class) - .withName("mode") - .withRequired(true) - .withDefaultValue("federated_login"))); - result.add(newDto(OAuthAuthenticatorDescriptor.class).withName(name).withLinks(links)); - } - return result; + public Set getRegisteredAuthenticators() throws ForbiddenException { + return oAuthAPI.getRegisteredAuthenticators(uriInfo); } /** @@ -166,47 +90,20 @@ public class OAuthAuthenticationService { @Path("token") @Produces(MediaType.APPLICATION_JSON) public OAuthToken token(@Required @QueryParam("oauth_provider") String oauthProvider) - throws ServerException, UnauthorizedException, NotFoundException, ForbiddenException { - OAuthAuthenticator provider = getAuthenticator(oauthProvider); - final Subject subject = EnvironmentContext.getCurrent().getSubject(); - try { - OAuthToken token = provider.getToken(subject.getUserId()); - if (token == null) { - token = provider.getToken(subject.getUserName()); - } - if (token != null) { - return token; - } - throw new UnauthorizedException( - "OAuth token for user " + subject.getUserId() + " was not found"); - } catch (IOException e) { - throw new ServerException(e.getLocalizedMessage(), e); - } + throws ServerException, UnauthorizedException, NotFoundException, ForbiddenException, + BadRequestException, ConflictException { + return oAuthAPI.getToken(oauthProvider); } + /** + * Invalidate OAuth token for user. + * + * @param oauthProvider OAuth provider name + */ @DELETE @Path("token") public void invalidate(@Required @QueryParam("oauth_provider") String oauthProvider) throws UnauthorizedException, NotFoundException, ServerException, ForbiddenException { - - OAuthAuthenticator oauth = getAuthenticator(oauthProvider); - final Subject subject = EnvironmentContext.getCurrent().getSubject(); - try { - if (!oauth.invalidateToken(subject.getUserId())) { - throw new UnauthorizedException( - "OAuth token for user " + subject.getUserId() + " was not found"); - } - } catch (IOException e) { - throw new ServerException(e.getMessage()); - } - } - - protected OAuthAuthenticator getAuthenticator(String oauthProviderName) throws NotFoundException { - OAuthAuthenticator oauth = providers.getAuthenticator(oauthProviderName); - if (oauth == null) { - LOG.warn("Unsupported OAuth provider {} ", oauthProviderName); - throw new NotFoundException("Unsupported OAuth provider " + oauthProviderName); - } - return oauth; + oAuthAPI.invalidateToken(oauthProvider); } } diff --git a/wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPITest.java b/wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPITest.java new file mode 100644 index 0000000000..aeb13d70cd --- /dev/null +++ b/wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/EmbeddedOAuthAPITest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.security.oauth; + +import static org.eclipse.che.dto.server.DtoFactory.newDto; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +import org.eclipse.che.api.auth.shared.dto.OAuthToken; +import org.eclipse.che.api.core.NotFoundException; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +/** @author Mykhailo Kuznietsov */ +@Listeners(value = MockitoTestNGListener.class) +public class EmbeddedOAuthAPITest { + + @Mock OAuthAuthenticatorProvider providers; + + @InjectMocks EmbeddedOAuthAPI embeddedOAuthAPI; + + @Test( + expectedExceptions = NotFoundException.class, + expectedExceptionsMessageRegExp = "Unsupported OAuth provider unknown" + ) + public void shouldThrowExceptionIfNoSuchProviderFound() throws Exception { + embeddedOAuthAPI.getToken("unknown"); + } + + @Test + public void shouldBeAbleToGetUserToken() throws Exception { + String provider = "myprovider"; + String token = "token123"; + OAuthAuthenticator authenticator = mock(OAuthAuthenticator.class); + when(providers.getAuthenticator(eq(provider))).thenReturn(authenticator); + + when(authenticator.getToken(anyString())).thenReturn(newDto(OAuthToken.class).withToken(token)); + + OAuthToken result = embeddedOAuthAPI.getToken(provider); + + assertEquals(result.getToken(), token); + } +} diff --git a/wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/OAuthAuthenticationServiceTest.java b/wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/OAuthAuthenticationServiceTest.java deleted file mode 100644 index 96e0cb0a67..0000000000 --- a/wsmaster/che-core-api-auth/src/test/java/org/eclipse/che/security/oauth/OAuthAuthenticationServiceTest.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2012-2018 Red Hat, Inc. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * Red Hat, Inc. - initial API and implementation - */ -package org.eclipse.che.security.oauth; - -import static com.jayway.restassured.RestAssured.given; -import static org.eclipse.che.dto.server.DtoFactory.newDto; -import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME; -import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD; -import static org.everrest.assured.JettyHttpServer.SECURE_PATH; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.testng.Assert.assertEquals; - -import com.jayway.restassured.response.Response; -import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.core.UriInfo; -import org.eclipse.che.api.auth.shared.dto.OAuthToken; -import org.eclipse.che.api.core.rest.ApiExceptionMapper; -import org.eclipse.che.api.core.rest.shared.dto.ServiceError; -import org.eclipse.che.commons.env.EnvironmentContext; -import org.eclipse.che.commons.subject.SubjectImpl; -import org.eclipse.che.dto.server.DtoFactory; -import org.everrest.assured.EverrestJetty; -import org.everrest.assured.JettyHttpServer; -import org.everrest.core.Filter; -import org.everrest.core.GenericContainerRequest; -import org.everrest.core.RequestFilter; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.testng.MockitoTestNGListener; -import org.testng.annotations.Listeners; -import org.testng.annotations.Test; - -/** @author Max Shaposhnik */ -@Listeners(value = {EverrestJetty.class, MockitoTestNGListener.class}) -public class OAuthAuthenticationServiceTest { - @SuppressWarnings("unused") - private EnvironmentFilter filter = new EnvironmentFilter(); - - @SuppressWarnings("unused") - private final ApiExceptionMapper exceptionMapper = new ApiExceptionMapper(); - - @Mock protected OAuthAuthenticatorProvider providers; - @Mock protected UriInfo uriInfo; - @Mock protected SecurityContext security; - @InjectMocks OAuthAuthenticationService service; - - @Filter - public static class EnvironmentFilter implements RequestFilter { - public void doFilter(GenericContainerRequest request) { - EnvironmentContext context = EnvironmentContext.getCurrent(); - context.setSubject( - new SubjectImpl(JettyHttpServer.ADMIN_USER_NAME, "id-2314", "token-2323", false)); - } - } - - @Test - public void shouldThrowExceptionIfNoSuchProviderFound() throws Exception { - final Response response = - given() - .auth() - .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) - .contentType("application/json") - .when() - .queryParam("oauth_provider", "unknown") - .get(SECURE_PATH + "/oauth/token"); - - assertEquals(response.getStatusCode(), 404); - assertEquals( - DtoFactory.getInstance() - .createDtoFromJson(response.getBody().asInputStream(), ServiceError.class) - .getMessage(), - "Unsupported OAuth provider unknown"); - } - - @Test - public void shouldBeAbleToGetUserToken() throws Exception { - String provider = "myprovider"; - String token = "token123"; - OAuthAuthenticator authenticator = mock(OAuthAuthenticator.class); - when(providers.getAuthenticator(eq(provider))).thenReturn(authenticator); - when(authenticator.getToken(anyString())).thenReturn(newDto(OAuthToken.class).withToken(token)); - - final Response response = - given() - .auth() - .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) - .contentType("application/json") - .when() - .queryParam("oauth_provider", provider) - .get(SECURE_PATH + "/oauth/token"); - - assertEquals(response.getStatusCode(), 200); - assertEquals( - DtoFactory.getInstance() - .createDtoFromJson(response.getBody().asInputStream(), OAuthToken.class) - .getToken(), - token); - } -}