/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * 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: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.security.oauth; import org.eclipse.che.api.auth.shared.dto.OAuthToken; import org.eclipse.che.api.core.BadRequestException; 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.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; import javax.inject.Inject; import javax.inject.Named; 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; 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 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 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; /** 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; @Context protected UriInfo uriInfo; @Context protected SecurityContext security; /** * 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> * * @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, BadRequestException, OAuthAuthenticationException { OAuthAuthenticator oauth = getAuthenticator(oauthProvider); final String authUrl = oauth.getAuthenticateUrl(getRequestUrl(uriInfo), scopes == null ? emptyList() : scopes); return Response.temporaryRedirect(URI.create(authUrl)).build(); } @GET @Path("callback") public Response callback(@QueryParam("errorValues") List<String> errorValues) throws OAuthAuthenticationException, BadRequestException { 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(); } /** * Gets list of installed OAuth authenticators. * * @return list of installed OAuth authenticators */ @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; } /** * Gets OAuth token for user. * * @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 ServerException, BadRequestException, 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 NotFoundException("OAuth token for user " + subject.getUserId() + " was not found"); } catch (IOException e) { throw new ServerException(e.getLocalizedMessage(), e); } } @DELETE @Path("token") public void invalidate(@Required @QueryParam("oauth_provider") String oauthProvider) throws BadRequestException, NotFoundException, ServerException, ForbiddenException { OAuthAuthenticator oauth = getAuthenticator(oauthProvider); final Subject subject = EnvironmentContext.getCurrent().getSubject(); try { if (!oauth.invalidateToken(subject.getUserId())) { throw new NotFoundException("OAuth token for user " + subject.getUserId() + " was not found"); } } catch (IOException e) { throw new ServerException(e.getMessage()); } } protected OAuthAuthenticator getAuthenticator(String oauthProviderName) throws BadRequestException { OAuthAuthenticator oauth = providers.getAuthenticator(oauthProviderName); if (oauth == null) { LOG.warn("Unsupported OAuth provider {} ", oauthProviderName); throw new BadRequestException("Unsupported OAuth provider " + oauthProviderName); } return oauth; } }