com.haulmont.restapi.idp.IdpAuthController.java Source code

Java tutorial

Introduction

Here is the source code for com.haulmont.restapi.idp.IdpAuthController.java

Source

/*
 * Copyright (c) 2008-2017 Haulmont.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.haulmont.restapi.idp;

import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import com.haulmont.bali.util.URLEncodeUtils;
import com.haulmont.cuba.core.global.Configuration;
import com.haulmont.cuba.core.sys.ConditionalOnAppProperty;
import com.haulmont.cuba.security.global.IdpSession;
import com.haulmont.restapi.auth.OAuthTokenIssuer;
import com.haulmont.restapi.auth.OAuthTokenIssuer.OAuth2AccessTokenResult;
import com.haulmont.restapi.config.RestApiConfig;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.ClientAuthenticationException;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.*;

@ConditionalOnAppProperty(property = "cuba.rest.idp.enabled", value = "true")
@RestController
public class IdpAuthController implements InitializingBean {

    public static final String IDP_SESSION_ID_TOKEN_ATTRIBUTE = "idp_session_id";

    private static final Logger log = LoggerFactory.getLogger(IdpAuthController.class);

    @Inject
    protected Configuration configuration;
    @Inject
    protected OAuthTokenIssuer oAuthTokenIssuer;

    protected RestIdpConfig idpConfig;
    protected RestApiConfig restApiConfig;

    protected String idpBaseURL;
    protected String idpTrustedServicePassword;
    protected String idpDefaultRedirectUrl;

    protected Set<HttpMethod> allowedRequestMethods = Collections.singleton(HttpMethod.POST);

    protected WebResponseExceptionTranslator providerExceptionHandler = new DefaultWebResponseExceptionTranslator();

    @GetMapping(value = "/v2/idp/token")
    public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal,
            @RequestParam Map<String, String> parameters, HttpServletRequest request)
            throws HttpRequestMethodNotSupportedException {

        if (!allowedRequestMethods.contains(HttpMethod.GET)) {
            throw new HttpRequestMethodNotSupportedException("GET");
        }

        return postAccessToken(principal, parameters, request);
    }

    @PostMapping(value = "/v2/idp/token")
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal,
            @RequestParam Map<String, String> parameters, HttpServletRequest request)
            throws HttpRequestMethodNotSupportedException {

        if (!idpConfig.getIdpEnabled()) {
            log.debug("IDP authentication is disabled. Property cuba.rest.idp.enabled is false");

            throw new InvalidGrantException("IDP is not supported");
        }

        if (!(principal instanceof Authentication)) {
            throw new InsufficientAuthenticationException(
                    "There is no client authentication. Try adding an appropriate authentication filter.");
        }

        // we cannot perform brute-force check here, since we don't know username

        String idpTicket = parameters.get("idp_ticket");
        String ipAddress = request.getRemoteAddr();

        OAuth2AccessTokenResult tokenResult = authenticate(idpTicket, request.getLocale(), ipAddress, parameters);

        return ResponseEntity.ok(tokenResult.getAccessToken());
    }

    @GetMapping(value = "/v2/idp/login")
    public ResponseEntity login(@RequestParam(value = "redirectUrl", required = false) String redirectUrl) {
        if (!idpConfig.getIdpEnabled()) {
            log.debug("IDP authentication is disabled. Property cuba.rest.idp.enabled is false");

            throw new InvalidGrantException("IDP is not supported");
        }

        if (redirectUrl == null) {
            redirectUrl = idpDefaultRedirectUrl;
        }

        if (redirectUrl == null) {
            log.debug("IDP defaultRedirectUrl is not set. Client did not provide redirectUrl parameter");

            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(new OAuth2Exception("Client did not provide redirectUrl parameter"));
        }

        return ResponseEntity.status(HttpStatus.FOUND).location(URI.create(getIdpLoginUrl(redirectUrl))).build();
    }

    @GetMapping(value = "/v2/idp/status")
    public ResponseEntity status() {
        if (!idpConfig.getIdpEnabled()) {
            log.debug("IDP authentication is disabled. Property cuba.rest.idp.enabled is false");

            throw new InvalidGrantException("IDP is not supported");
        }

        return ResponseEntity.status(HttpStatus.FOUND).location(URI.create(getIdpStatusUrl())).build();
    }

    protected String getIdpStatusUrl() {
        String idpBaseURL = this.idpBaseURL;
        if (!idpBaseURL.endsWith("/")) {
            idpBaseURL += "/";
        }
        return idpBaseURL + "status";
    }

    protected String getIdpLoginUrl(String redirectUrl) {
        String idpBaseURL = this.idpBaseURL;
        if (!idpBaseURL.endsWith("/")) {
            idpBaseURL += "/";
        }
        return idpBaseURL + "?response_type=client-ticket" + "&sp=" + URLEncodeUtils.encodeUtf8(redirectUrl);
    }

    protected OAuth2AccessTokenResult authenticate(String idpTicket, Locale locale, String ipAddress,
            Map<String, String> parameters) {
        IdpSession idpSession = getIdpSession(idpTicket);
        if (idpSession == null) {
            log.info("REST API authentication failed for IDP ticket: {} {}", idpTicket, ipAddress);
            throw new BadCredentialsException("Bad credentials");
        }

        if (restApiConfig.getStandardAuthenticationUsers().contains(idpSession.getLogin())) {
            log.info("User {} is not allowed to use external login in REST API", idpSession.getLogin());
            throw new BadCredentialsException("Bad credentials");
        }

        OAuthTokenIssuer.OAuth2AccessTokenRequest tokenRequest = new OAuthTokenIssuer.OAuth2AccessTokenRequest();
        tokenRequest.setLogin(idpSession.getLogin());
        tokenRequest.setLocale(locale);
        tokenRequest.setTokenDetails(ImmutableMap.of(IDP_SESSION_ID_TOKEN_ATTRIBUTE, idpSession.getId()));

        return oAuthTokenIssuer.issueToken(tokenRequest);
    }

    @Nullable
    protected IdpSession getIdpSession(String idpTicket) throws InvalidGrantException {
        String idpBaseURL = this.idpBaseURL;
        if (!idpBaseURL.endsWith("/")) {
            idpBaseURL += "/";
        }
        String idpTicketActivateUrl = idpBaseURL + "service/activate";

        HttpPost httpPost = new HttpPost(idpTicketActivateUrl);
        httpPost.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType());

        UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(
                Arrays.asList(new BasicNameValuePair("serviceProviderTicket", idpTicket),
                        new BasicNameValuePair("trustedServicePassword", idpTrustedServicePassword)),
                StandardCharsets.UTF_8);

        httpPost.setEntity(formEntity);

        HttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager();
        HttpClient client = HttpClientBuilder.create().setConnectionManager(connectionManager).build();

        String idpResponse;
        try {
            HttpResponse httpResponse = client.execute(httpPost);

            int statusCode = httpResponse.getStatusLine().getStatusCode();
            if (statusCode == 410) {
                // used old ticket
                return null;
            }

            if (statusCode != 200) {
                throw new RuntimeException("Idp respond with status " + statusCode);
            }

            idpResponse = new BasicResponseHandler().handleResponse(httpResponse);
        } catch (IOException e) {
            throw new RuntimeException("Unable to connect to IDP", e);
        } finally {
            connectionManager.shutdown();
        }

        IdpSession session;
        try {
            session = new Gson().fromJson(idpResponse, IdpSession.class);
        } catch (JsonSyntaxException e) {
            throw new RuntimeException("Unable to parse idp response", e);
        }

        return session;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        this.idpConfig = configuration.getConfig(RestIdpConfig.class);
        this.restApiConfig = configuration.getConfig(RestApiConfig.class);

        if (idpConfig.getIdpEnabled()) {
            checkRequiredConfigProperties(idpConfig);

            if (this.idpBaseURL == null) {
                this.idpBaseURL = idpConfig.getIdpBaseURL();
            }
            if (this.idpTrustedServicePassword == null) {
                this.idpTrustedServicePassword = idpConfig.getIdpTrustedServicePassword();
            }
            if (this.idpDefaultRedirectUrl == null) {
                this.idpDefaultRedirectUrl = idpConfig.getIdpDefaultRedirectUrl();
            }
        }
    }

    protected void checkRequiredConfigProperties(RestIdpConfig idpConfig) {
        List<String> missingProperties = new ArrayList<>();

        if (StringUtils.isBlank(idpConfig.getIdpBaseURL())) {
            missingProperties.add("cuba.rest.idp.baseUrl");
        }
        if (StringUtils.isBlank(idpConfig.getIdpTrustedServicePassword())) {
            missingProperties.add("cuba.rest.idp.trustedServicePassword");
        }
        if (!missingProperties.isEmpty()) {
            throw new IllegalStateException(
                    "Please configure required application properties for IDP integration: \n"
                            + StringUtils.join(missingProperties, "\n"));
        }
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ResponseEntity<OAuth2Exception> handleMissingServletRequestParameterException(Exception e)
            throws Exception {
        log.debug("Missing parameter in IDP auth controller: {}", e.getMessage());
        return new ResponseEntity<>(new OAuth2Exception(e.getMessage(), e), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
        log.error("Exception in IDP auth controller", e);
        return new ResponseEntity<>(new OAuth2Exception("Server error", e), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(BadCredentialsException.class)
    public ResponseEntity<OAuth2Exception> handleBadCredentialsException(BadCredentialsException e)
            throws Exception {
        return getExceptionTranslator().translate(e);
    }

    @ExceptionHandler(ClientAuthenticationException.class)
    public ResponseEntity<OAuth2Exception> handleClientAuthenticationException(ClientAuthenticationException e)
            throws Exception {
        return getExceptionTranslator().translate(e);
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResponseEntity<OAuth2Exception> handleHttpRequestMethodNotSupportedException(
            HttpRequestMethodNotSupportedException e) throws Exception {
        log.info("Handling error: {}, {}", e.getClass().getSimpleName(), e.getMessage());
        return getExceptionTranslator().translate(e);
    }

    public WebResponseExceptionTranslator getExceptionTranslator() {
        return providerExceptionHandler;
    }

    public void setExceptionTranslator(WebResponseExceptionTranslator providerExceptionHandler) {
        this.providerExceptionHandler = providerExceptionHandler;
    }

    public Set<HttpMethod> getAllowedRequestMethods() {
        return allowedRequestMethods;
    }

    public void setAllowedRequestMethods(Set<HttpMethod> allowedRequestMethods) {
        this.allowedRequestMethods = allowedRequestMethods;
    }

    public String getIdpBaseURL() {
        return idpBaseURL;
    }

    public void setIdpBaseURL(String idpBaseURL) {
        this.idpBaseURL = idpBaseURL;
    }

    public String getIdpTrustedServicePassword() {
        return idpTrustedServicePassword;
    }

    public void setIdpTrustedServicePassword(String idpTrustedServicePassword) {
        this.idpTrustedServicePassword = idpTrustedServicePassword;
    }

    public String getIdpDefaultRedirectUrl() {
        return idpDefaultRedirectUrl;
    }

    public void setIdpDefaultRedirectUrl(String idpDefaultRedirectUrl) {
        this.idpDefaultRedirectUrl = idpDefaultRedirectUrl;
    }
}