org.cloudfoundry.identity.client.UaaContextFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.cloudfoundry.identity.client.UaaContextFactory.java

Source

/*
 * *****************************************************************************
 *      Cloud Foundry
 *      Copyright (c) [2009-2015] Pivotal Software, Inc. All Rights Reserved.
 *      This product is licensed to you under the Apache License, Version 2.0 (the "License").
 *      You may not use this product except in compliance with the License.
 *
 *      This product includes a number of subcomponents with
 *      separate copyright notices and license terms. Your use of these
 *      subcomponents is subject to the terms and conditions of the
 *      subcomponent's license, as noted in the LICENSE file.
 * *****************************************************************************
 */

package org.cloudfoundry.identity.client;

import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolException;
import org.apache.http.client.RedirectStrategy;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HttpContext;
import org.cloudfoundry.identity.client.token.TokenRequest;
import org.cloudfoundry.identity.uaa.oauth.token.CompositeAccessToken;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.AccessTokenProviderChain;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.UnsupportedGrantTypeException;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpMessageConverterExtractor;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import javax.net.ssl.SSLContext;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static java.lang.String.format;
import static org.cloudfoundry.identity.client.token.GrantType.AUTHORIZATION_CODE;
import static org.cloudfoundry.identity.client.token.GrantType.PASSWORD_WITH_PASSCODE;
import static org.springframework.security.oauth2.common.AuthenticationScheme.header;

public class UaaContextFactory {

    /**
     * UAA Base URI
     */
    private final URI uaaURI;

    /**
     * Instantiates a context factory to authenticate against the UAA
     * @param uaaURI the UAA base URI
     */
    private UaaContextFactory(URI uaaURI) {
        this.uaaURI = uaaURI;
    }

    private String tokenPath = "/oauth/token";
    private String authorizePath = "/oauth/authorize";

    /**
     * Instantiates a context factory to authenticate against the UAA
     * The default token path, /oauth/token, and authorize path, /oauth/authorize are set.
     * @param uaaURI the UAA base URI
     */
    public static UaaContextFactory factory(URI uaaURI) {
        return new UaaContextFactory(uaaURI);
    }

    /**
     * Sets the token endpoint path. If not invoked, the default is /oauth/token
     * @param path the path for the token endpoint.
     * @return this mutable object
     */
    public UaaContextFactory tokenPath(String path) {
        this.tokenPath = path;
        return this;
    }

    /**
     * Sets the authorize endpoint path. If not invoked, the default is /oauth/authorize
     * @param path the path for the authorize endpoint.
     * @return this mutable object
     */
    public UaaContextFactory authorizePath(String path) {
        this.authorizePath = path;
        return this;
    }

    /**
     * Returns the authorize URI
     * @return the UAA authorization URI
     */
    public URI getAuthorizeUri() {
        UriComponentsBuilder authorizationURI = UriComponentsBuilder.newInstance();
        authorizationURI.uri(uaaURI);
        authorizationURI.path(authorizePath);
        return authorizationURI.build().toUri();
    }

    /**
     * Returns the URI
     * @return
     */
    public URI getTokenUri() {
        UriComponentsBuilder tokenURI = UriComponentsBuilder.newInstance();
        tokenURI.uri(uaaURI);
        tokenURI.path(tokenPath);
        return tokenURI.build().toUri();
    }

    /**
     * Creates a new {@link TokenRequest} object.
     * The object will have the token an authorize endpoints already configured.
     * @return the new token request that can be used for an access token request.
     */
    public TokenRequest tokenRequest() {
        return new TokenRequest(getTokenUri(), getAuthorizeUri());
    }

    /**
     * Authenticates the client and optionally the user and retrieves an access token
     * Token request must be valid, see {@link TokenRequest#isValid()}
     * @param request - a fully configured token request
     * @return an authenticated UAA context with
     * @throws NullPointerException if the request object is null
     * @throws IllegalArgumentException if the token request is invalid
     */
    public UaaContext authenticate(TokenRequest request) {
        if (request == null) {
            throw new NullPointerException(TokenRequest.class.getName() + " cannot be null.");
        }
        if (!request.isValid()) {
            throw new IllegalArgumentException("Invalid token request.");
        }
        switch (request.getGrantType()) {
        case CLIENT_CREDENTIALS:
            return authenticateClientCredentials(request);
        case PASSWORD:
        case PASSWORD_WITH_PASSCODE:
            return authenticatePassword(request);
        case AUTHORIZATION_CODE:
            return authenticateAuthCode(request);
        case AUTHORIZATION_CODE_WITH_TOKEN:
            return authenticateAuthCodeWithToken(request);
        case FETCH_TOKEN_FROM_CODE:
            return fetchTokenFromCode(request);
        case SAML2_BEARER:
            return authenticateSaml2BearerAssertion(request);
        default:
            throw new UnsupportedGrantTypeException("Not implemented:" + request.getGrantType());
        }
    }

    protected UaaContext fetchTokenFromCode(final TokenRequest request) {
        String clientBasicAuth = getClientBasicAuthHeader(request);

        RestTemplate template = new RestTemplate();
        if (request.isSkipSslValidation()) {
            template.setRequestFactory(getNoValidatingClientHttpRequestFactory());
        }
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.AUTHORIZATION, clientBasicAuth);
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        form.add(OAuth2Utils.GRANT_TYPE, "authorization_code");
        form.add(OAuth2Utils.REDIRECT_URI, request.getRedirectUri().toString());
        String responseType = "token";
        if (request.wantsIdToken()) {
            responseType += " id_token";
        }
        form.add(OAuth2Utils.RESPONSE_TYPE, responseType);
        form.add("code", request.getAuthorizationCode());

        ResponseEntity<CompositeAccessToken> token = template.exchange(request.getTokenEndpoint(), HttpMethod.POST,
                new HttpEntity<>(form, headers), CompositeAccessToken.class);
        return new UaaContextImpl(request, null, token.getBody());
    }

    /**
     * Not yet implemented
     * @param tokenRequest - a configured TokenRequest
     * @return an authenticated {@link UaaContext}
     */
    protected UaaContext authenticateAuthCode(final TokenRequest tokenRequest) {
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setPreEstablishedRedirectUri(tokenRequest.getRedirectUri().toString());
        details.setUserAuthorizationUri(tokenRequest.getAuthorizationEndpoint().toString());
        configureResourceDetails(tokenRequest, details);
        setClientCredentials(tokenRequest, details);
        setRequestScopes(tokenRequest, details);

        //begin - work around for not having UI for now
        DefaultOAuth2ClientContext oAuth2ClientContext = new DefaultOAuth2ClientContext();
        oAuth2ClientContext.getAccessTokenRequest().setStateKey(tokenRequest.getState());
        oAuth2ClientContext.setPreservedState(tokenRequest.getState(), details.getPreEstablishedRedirectUri());
        oAuth2ClientContext.getAccessTokenRequest().setCurrentUri(details.getPreEstablishedRedirectUri());
        //end - work around for not having UI for now

        OAuth2RestTemplate template = new OAuth2RestTemplate(details, oAuth2ClientContext);
        skipSslValidation(tokenRequest, template, null);
        template.getAccessToken();
        throw new UnsupportedOperationException(AUTHORIZATION_CODE + " is not yet implemented");
    }

    /**
     * Performs and authorization_code grant, but uses a token to assert the user's identity.
     * @param tokenRequest - a configured TokenRequest
     * @return an authenticated {@link UaaContext}
     */
    protected UaaContext authenticateAuthCodeWithToken(final TokenRequest tokenRequest) {
        List<OAuth2AccessTokenSupport> providers = Collections
                .singletonList(new AuthorizationCodeAccessTokenProvider() {
                    @Override
                    protected ResponseExtractor<OAuth2AccessToken> getResponseExtractor() {
                        getRestTemplate(); // force initialization
                        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
                        return new HttpMessageConverterExtractor<OAuth2AccessToken>(CompositeAccessToken.class,
                                Arrays.asList(converter));
                    }
                });
        enhanceRequestParameters(tokenRequest, providers.get(0));
        AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
        details.setPreEstablishedRedirectUri(tokenRequest.getRedirectUri().toString());
        configureResourceDetails(tokenRequest, details);
        setClientCredentials(tokenRequest, details);
        setRequestScopes(tokenRequest, details);
        details.setUserAuthorizationUri(tokenRequest.getAuthorizationEndpoint().toString());
        DefaultOAuth2ClientContext oAuth2ClientContext = new DefaultOAuth2ClientContext();
        oAuth2ClientContext.getAccessTokenRequest().setStateKey(tokenRequest.getState());
        oAuth2ClientContext.setPreservedState(tokenRequest.getState(), details.getPreEstablishedRedirectUri());
        oAuth2ClientContext.getAccessTokenRequest().setCurrentUri(details.getPreEstablishedRedirectUri());
        Map<String, List<String>> headers = (Map<String, List<String>>) oAuth2ClientContext.getAccessTokenRequest()
                .getHeaders();
        headers.put("Authorization", Arrays.asList("bearer " + tokenRequest.getAuthCodeAPIToken()));
        OAuth2RestTemplate template = new OAuth2RestTemplate(details, oAuth2ClientContext);
        skipSslValidation(tokenRequest, template, providers);
        OAuth2AccessToken token = template.getAccessToken();
        return new UaaContextImpl(tokenRequest, template, (CompositeAccessToken) token);
    }

    protected UaaContext authenticateSaml2BearerAssertion(final TokenRequest request) {
        RestTemplate template = new RestTemplate();
        if (request.isSkipSslValidation()) {
            template.setRequestFactory(getNoValidatingClientHttpRequestFactory());
        }
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        form.add(OAuth2Utils.CLIENT_ID, request.getClientId());
        form.add("client_secret", request.getClientSecret());
        form.add(OAuth2Utils.GRANT_TYPE, "urn:ietf:params:oauth:grant-type:saml2-bearer");
        form.add("assertion", request.getAuthCodeAPIToken());

        ResponseEntity<CompositeAccessToken> token = template.exchange(request.getTokenEndpoint(), HttpMethod.POST,
                new HttpEntity<>(form, headers), CompositeAccessToken.class);
        return new UaaContextImpl(request, null, token.getBody());
    }

    protected String getClientBasicAuthHeader(TokenRequest request) {
        try {
            byte[] autbytes = Base64
                    .encode(format("%s:%s", request.getClientId(), request.getClientSecret()).getBytes("UTF-8"));
            String base64 = new String(autbytes);
            return format("Basic %s", base64);
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * Performs a {@link org.cloudfoundry.identity.client.token.GrantType#PASSWORD authentication}
     * @param tokenRequest - a configured TokenRequest
     * @return an authenticated {@link UaaContext}
     */
    protected UaaContext authenticatePassword(final TokenRequest tokenRequest) {
        List<OAuth2AccessTokenSupport> providers = Collections
                .singletonList(new ResourceOwnerPasswordAccessTokenProvider() {
                    @Override
                    protected ResponseExtractor<OAuth2AccessToken> getResponseExtractor() {
                        getRestTemplate(); // force initialization
                        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
                        return new HttpMessageConverterExtractor<OAuth2AccessToken>(CompositeAccessToken.class,
                                Arrays.asList(converter));
                    }
                });
        enhanceRequestParameters(tokenRequest, providers.get(0));
        ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
        configureResourceDetails(tokenRequest, details);
        setUserCredentials(tokenRequest, details);
        setClientCredentials(tokenRequest, details);
        setRequestScopes(tokenRequest, details);
        OAuth2RestTemplate template = new OAuth2RestTemplate(details, new DefaultOAuth2ClientContext());
        skipSslValidation(tokenRequest, template, providers);
        OAuth2AccessToken token = template.getAccessToken();
        return new UaaContextImpl(tokenRequest, template, (CompositeAccessToken) token);
    }

    /**
     * Adds a request enhancer to the provider.
     * Currently only two request parameters are being enhanced
     * 1. If the {@link TokenRequest} wants an id_token the <code>id_token token</code> values are added as a response_type parameter
     * 2. If the {@link TokenRequest} is a {@link org.cloudfoundry.identity.client.token.GrantType#PASSWORD_WITH_PASSCODE}
     * the <code>passcode</code> parameter will be added to the request
     * @param tokenRequest the token request, expected to be a password grant
     * @param provider the provider to enhance
     */
    protected void enhanceRequestParameters(TokenRequest tokenRequest, OAuth2AccessTokenSupport provider) {
        provider.setTokenRequestEnhancer( //add id_token to the response type if requested.
                (AccessTokenRequest request, OAuth2ProtectedResourceDetails resource,
                        MultiValueMap<String, String> form, HttpHeaders headers) -> {
                    if (tokenRequest.wantsIdToken()) {
                        form.put(OAuth2Utils.RESPONSE_TYPE, Arrays.asList("id_token token"));
                    }
                    if (tokenRequest.getGrantType() == PASSWORD_WITH_PASSCODE) {
                        form.put("passcode", Arrays.asList(tokenRequest.getPasscode()));
                    }
                });
    }

    /**
     * Performs a {@link org.cloudfoundry.identity.client.token.GrantType#CLIENT_CREDENTIALS authentication}
     * @param request - a configured TokenRequest
     * @return an authenticated {@link UaaContext}
     */

    protected UaaContext authenticateClientCredentials(TokenRequest request) {
        ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
        configureResourceDetails(request, details);
        setClientCredentials(request, details);
        setRequestScopes(request, details);
        OAuth2RestTemplate template = new OAuth2RestTemplate(details, new DefaultOAuth2ClientContext());
        skipSslValidation(request, template, null);
        OAuth2AccessToken token = template.getAccessToken();
        CompositeAccessToken result = new CompositeAccessToken(token);
        return new UaaContextImpl(request, template, result);
    }

    /**
     * Sets the token endpoint on the resource details
     * Sets the authentication scheme to be {@link org.springframework.security.oauth2.common.AuthenticationScheme#header}
     * @param tokenRequest the token request containing the token endpoint
     * @param details the details object that will be configured
     */
    protected void configureResourceDetails(TokenRequest tokenRequest, BaseOAuth2ProtectedResourceDetails details) {
        details.setAuthenticationScheme(header);
        details.setAccessTokenUri(tokenRequest.getTokenEndpoint().toString());
    }

    /**
     * Sets the requested scopes on the resource details, if and only if the requested scopes are not null
     * @param tokenRequest the token request containing the requested scopes, if any
     * @param details the details object that will be configured
     */
    protected void setRequestScopes(TokenRequest tokenRequest, BaseOAuth2ProtectedResourceDetails details) {
        if (!Objects.isNull(tokenRequest.getScopes())) {
            details.setScope(new LinkedList(tokenRequest.getScopes()));
        }
    }

    /**
     * Sets the client_id and client_secret on the resource details object
     * @param tokenRequest the token request containing the client_id and client_secret
     * @param details the details object that. will be configured
     */
    protected void setClientCredentials(TokenRequest tokenRequest, BaseOAuth2ProtectedResourceDetails details) {
        details.setClientId(tokenRequest.getClientId());
        details.setClientSecret(tokenRequest.getClientSecret());
    }

    /**
     * Sets the username and password on the resource details object
     * @param tokenRequest the token request containing the client_id and client_secret
     * @param details the details object that. will be configured
     */
    protected void setUserCredentials(TokenRequest tokenRequest, ResourceOwnerPasswordResourceDetails details) {
        details.setUsername(tokenRequest.getUsername());
        details.setPassword(tokenRequest.getPassword());
    }

    /**
     * If the {@link TokenRequest#isSkipSslValidation()} returns true, the rest template
     * will be configured
     * @param tokenRequest
     * @param template
     */
    protected void skipSslValidation(TokenRequest tokenRequest, OAuth2RestTemplate template,
            List<OAuth2AccessTokenSupport> existingProviders) {
        ClientHttpRequestFactory requestFactory = null;
        if (tokenRequest.isSkipSslValidation()) {
            requestFactory = getNoValidatingClientHttpRequestFactory();
        }
        List<OAuth2AccessTokenSupport> accessTokenProviders = existingProviders != null ? existingProviders
                : Arrays.<OAuth2AccessTokenSupport>asList(new AuthorizationCodeAccessTokenProvider(),
                        new ImplicitAccessTokenProvider(), new ResourceOwnerPasswordAccessTokenProvider(),
                        new ClientCredentialsAccessTokenProvider());
        List<AccessTokenProvider> providers = new ArrayList<>();
        for (OAuth2AccessTokenSupport provider : accessTokenProviders) {
            if (requestFactory != null) {
                provider.setRequestFactory(requestFactory);
            }
            providers.add((AccessTokenProvider) provider);
        }
        AccessTokenProviderChain chain = new AccessTokenProviderChain(providers);
        template.setAccessTokenProvider(chain);
    }

    public static ClientHttpRequestFactory getNoValidatingClientHttpRequestFactory() {
        return getNoValidatingClientHttpRequestFactory(true);
    }

    public static ClientHttpRequestFactory getNoValidatingClientHttpRequestFactory(boolean followRedirects) {
        ClientHttpRequestFactory requestFactory;
        SSLContext sslContext;
        try {
            sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (KeyManagementException e) {
            throw new RuntimeException(e);
        } catch (KeyStoreException e) {
            throw new RuntimeException(e);
        }
        //
        CloseableHttpClient httpClient = HttpClients.custom().setSslcontext(sslContext)
                .setRedirectStrategy(followRedirects ? new DefaultRedirectStrategy() : new RedirectStrategy() {
                    @Override
                    public boolean isRedirected(HttpRequest request, HttpResponse response, HttpContext context)
                            throws ProtocolException {
                        return false;
                    }

                    @Override
                    public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response,
                            HttpContext context) throws ProtocolException {
                        return null;
                    }
                }).build();

        requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
        return requestFactory;
    }

}