Add ability to configure OAuth type in Multi User Che (#9640)
parent
5c4fd8b698
commit
cd99cf4e6c
|
|
@ -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<String, String> 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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@
|
|||
<groupId>org.eclipse.che.core</groupId>
|
||||
<artifactId>che-core-api-account</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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<OAuthAPI> {
|
||||
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\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> 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<String> errorValues) throws ForbiddenException {
|
||||
throw new ForbiddenException("Method is not supported in this implementation of OAuth API");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<OAuthAuthenticatorDescriptor> getRegisteredAuthenticators(UriInfo uriInfo)
|
||||
throws ForbiddenException {
|
||||
throw new ForbiddenException("Method is not supported in this implementation of OAuth API");
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> 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<String> errorValues)
|
||||
throws NotFoundException, OAuthAuthenticationException {
|
||||
URL requestUrl = getRequestUrl(uriInfo);
|
||||
Map<String, List<String>> 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<String> scopes = params.get("scope");
|
||||
oauth.callback(requestUrl, scopes == null ? Collections.<String>emptyList() : scopes);
|
||||
final String redirectAfterLogin = getParameter(params, "redirect_after_login");
|
||||
return Response.temporaryRedirect(URI.create(redirectAfterLogin)).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<OAuthAuthenticatorDescriptor> getRegisteredAuthenticators(UriInfo uriInfo) {
|
||||
Set<OAuthAuthenticatorDescriptor> result = new HashSet<>();
|
||||
final UriBuilder uriBuilder =
|
||||
uriInfo.getBaseUriBuilder().clone().path(OAuthAuthenticationService.class);
|
||||
for (String name : providers.getRegisteredProviderNames()) {
|
||||
final List<Link> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> scopes,
|
||||
String redirectAfterLogin,
|
||||
HttpServletRequest request)
|
||||
throws NotFoundException, OAuthAuthenticationException, ForbiddenException,
|
||||
BadRequestException;
|
||||
|
||||
/** Implementation of method {@link OAuthAuthenticationService#callback(List)} */
|
||||
Response callback(UriInfo uriInfo, List<String> errorValues)
|
||||
throws NotFoundException, OAuthAuthenticationException, ForbiddenException;
|
||||
|
||||
/** Implementation of method {@link OAuthAuthenticationService#getRegisteredAuthenticators()} */
|
||||
Set<OAuthAuthenticatorDescriptor> 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;
|
||||
}
|
||||
|
|
@ -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:
|
||||
*
|
||||
* <table>
|
||||
* <tr><th>Name</th><th>Description</th><th>Mandatory</th><th>Default value</th></tr>
|
||||
* <tr><td>oauth_provider</td><td>Name of OAuth provider. At the moment <tt>google</tt> and <tt>github</tt>
|
||||
* supported</td><td>yes</td><td>none</td></tr>
|
||||
* <tr><td>scope</td><td>Specify 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.</td><td>no</td><td>Empty list</td></tr>
|
||||
* <tr><td>mode</td><td>Authentication mode. May be <tt>federated_login</tt> or <tt>token</tt>. If <tt>mode</tt>
|
||||
* set
|
||||
* as <tt>federated_login</tt> 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.</td><td>no</td><td>token</td></tr>
|
||||
* <tr><td>redirect_after_login</td><td>URL for user redirection after successful
|
||||
* authentication</td><td>yes</td><td>none</td></tr>
|
||||
* </table>
|
||||
* 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<String> 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<String> 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<String> errorValues)
|
||||
throws OAuthAuthenticationException, NotFoundException {
|
||||
URL requestUrl = getRequestUrl(uriInfo);
|
||||
Map<String, List<String>> 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<String> scopes = params.get("scope");
|
||||
oauth.callback(requestUrl, scopes == null ? Collections.<String>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<OAuthAuthenticatorDescriptor> getRegisteredAuthenticators() {
|
||||
Set<OAuthAuthenticatorDescriptor> result = new HashSet<>();
|
||||
final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder().clone().path(getClass());
|
||||
for (String name : providers.getRegisteredProviderNames()) {
|
||||
final List<Link> 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<OAuthAuthenticatorDescriptor> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue