com.emergya.spring.security.oauth.google.GoogleAuthorizationCodeAccessTokenProvider.java Source code

Java tutorial

Introduction

Here is the source code for com.emergya.spring.security.oauth.google.GoogleAuthorizationCodeAccessTokenProvider.java

Source

/*
 * Copyright 2002-2011 the original author or authors.
 *
 * 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.
 */
/*
* Copyright 2002-2011 the original author or authors.
*
* 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.emergya.spring.security.oauth.google;

import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.oauth2.client.filter.state.DefaultStateKeyGenerator;
import org.springframework.security.oauth2.client.filter.state.StateKeyGenerator;
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.UserApprovalRequiredException;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.DefaultRequestEnhancer;
import org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport;
import org.springframework.security.oauth2.client.token.RequestEnhancer;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResponseExtractor;

/**
 * Modified AuthorizationCodeAccessTokenProvider for obtaining an oauth2 access token by using an authorization code, using google
 * custom resource details.
 *
 * @author Ryan Heaton
 * @author Dave Syer
 * @author lroman
 */
public class GoogleAuthorizationCodeAccessTokenProvider extends OAuth2AccessTokenSupport
        implements AccessTokenProvider {

    private StateKeyGenerator stateKeyGenerator = new DefaultStateKeyGenerator();

    private String scopePrefix = OAuth2Utils.SCOPE_PREFIX;

    private RequestEnhancer authorizationRequestEnhancer = new DefaultRequestEnhancer();

    /**
     * A custom enhancer for the authorization request.
     *
     * @param authorizationRequestEnhancer the authorization request enhancer to set.
     */
    public final void setAuthorizationRequestEnhancer(final RequestEnhancer authorizationRequestEnhancer) {
        this.authorizationRequestEnhancer = authorizationRequestEnhancer;
    }

    /**
     * Prefix for scope approval parameters.
     *
     * @param scopePrefix the scope prefix to set.
     */
    public final void setScopePrefix(final String scopePrefix) {
        this.scopePrefix = scopePrefix;
    }

    /**
     * @param stateKeyGenerator the stateKeyGenerator to set
     */
    public final void setStateKeyGenerator(final StateKeyGenerator stateKeyGenerator) {
        this.stateKeyGenerator = stateKeyGenerator;
    }

    @Override
    public final boolean supportsResource(final OAuth2ProtectedResourceDetails resource) {
        return resource instanceof AuthorizationCodeResourceDetails
                && "authorization_code".equals(resource.getGrantType());
    }

    @Override
    public final boolean supportsRefresh(final OAuth2ProtectedResourceDetails resource) {
        return supportsResource(resource);
    }

    /**
     * Obtains the authorization code from the access token request.
     *
     * @param details the authenticatoin details
     * @param request the access token request
     * @return the authorization code
     * @throws UserRedirectRequiredException when redirection is required
     * @throws UserApprovalRequiredException when the user requires approval
     * @throws AccessDeniedException when the user is denied access
     * @throws OAuth2AccessDeniedException when the user is denied access but we dont want the default Spring Security handling
     */
    public final String obtainAuthorizationCode(final OAuth2ProtectedResourceDetails details,
            final AccessTokenRequest request) throws UserRedirectRequiredException, UserApprovalRequiredException,
            AccessDeniedException, OAuth2AccessDeniedException {

        GoogleAuthCodeResourceDetails resource;

        try {
            resource = (GoogleAuthCodeResourceDetails) details;
        } catch (ClassCastException ex) {
            throw new IllegalArgumentException("details is not an instance of class GoogleAuthCodeResourceDetails");
        }

        HttpHeaders headers = getHeadersForAuthorizationRequest(request);
        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        if (request.containsKey(OAuth2Utils.USER_OAUTH_APPROVAL)) {
            form.set(OAuth2Utils.USER_OAUTH_APPROVAL, request.getFirst(OAuth2Utils.USER_OAUTH_APPROVAL));
            for (String scope : details.getScope()) {
                form.set(scopePrefix + scope, request.getFirst(OAuth2Utils.USER_OAUTH_APPROVAL));
            }
        } else {
            form.putAll(getParametersForAuthorizeRequest(resource, request));
        }
        authorizationRequestEnhancer.enhance(request, resource, form, headers);
        final AccessTokenRequest copy = request;

        final ResponseExtractor<ResponseEntity<Void>> delegate = getAuthorizationResponseExtractor();
        ResponseExtractor<ResponseEntity<Void>> extractor = new CookieResponseExtractor(copy, delegate);
        // Instead of using restTemplate.exchange we use an explicit response extractor here so it can be overridden by
        // subclasses
        ResponseEntity<Void> response = getRestTemplate().execute(resource.getUserAuthorizationUri(),
                HttpMethod.POST, getRequestCallback(resource, form, headers), extractor, form.toSingleValueMap());

        if (response.getStatusCode() == HttpStatus.OK) {
            // Need to re-submit with approval...
            throw getUserApprovalSignal(resource, request);
        }

        URI location = response.getHeaders().getLocation();
        String query = location.getQuery();
        Map<String, String> map = OAuth2Utils.extractMap(query);
        if (map.containsKey("state")) {
            request.setStateKey(map.get("state"));
            if (request.getPreservedState() == null) {
                String redirectUri = resource.getRedirectUri(request);
                if (redirectUri != null) {
                    request.setPreservedState(redirectUri);
                } else {
                    request.setPreservedState(new Object());
                }
            }
        }

        String code = map.get("code");
        if (code == null) {
            throw new UserRedirectRequiredException(location.toString(), form.toSingleValueMap());
        }
        request.set("code", code);
        return code;

    }

    /**
     * Gets the authorization response extractor object.
     *
     * @return the authorizatoin response extractor.
     */
    protected final ResponseExtractor<ResponseEntity<Void>> getAuthorizationResponseExtractor() {
        return new AuthResponseExtractor();
    }

    @Override
    public final OAuth2AccessToken obtainAccessToken(final OAuth2ProtectedResourceDetails details,
            final AccessTokenRequest request) throws UserRedirectRequiredException, UserApprovalRequiredException,
            AccessDeniedException, OAuth2AccessDeniedException {

        GoogleAuthCodeResourceDetails resource;

        try {
            resource = (GoogleAuthCodeResourceDetails) details;
        } catch (ClassCastException ex) {
            throw new IllegalArgumentException("details is not an instance of class GoogleAuthCodeResourceDetails");
        }

        if (request.getAuthorizationCode() == null) {
            if (request.getStateKey() == null) {
                throw getRedirectForAuthorization(resource, request);
            }
            obtainAuthorizationCode(resource, request);
        }
        return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
                getHeadersForTokenRequest());

    }

    @Override
    public final OAuth2AccessToken refreshAccessToken(final OAuth2ProtectedResourceDetails resource,
            final OAuth2RefreshToken refreshToken, final AccessTokenRequest request)
            throws UserRedirectRequiredException, OAuth2AccessDeniedException {
        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        form.add("grant_type", "refresh_token");
        form.add("refresh_token", refreshToken.getValue());
        try {
            return retrieveToken(request, resource, form, getHeadersForTokenRequest());
        } catch (OAuth2AccessDeniedException e) {
            try {
                throw getRedirectForAuthorization((GoogleAuthCodeResourceDetails) resource, request);
            } catch (ClassCastException ex) {
                throw new IllegalArgumentException(
                        "details is not an instance of class GoogleAuthCodeResourceDetails");
            }
        }
    }

    private HttpHeaders getHeadersForTokenRequest() {
        HttpHeaders headers = new HttpHeaders();
        // No cookie for token request
        return headers;
    }

    private HttpHeaders getHeadersForAuthorizationRequest(final AccessTokenRequest request) {
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(request.getHeaders());
        if (request.getCookie() != null) {
            headers.set("Cookie", request.getCookie());
        }
        return headers;
    }

    private MultiValueMap<String, String> getParametersForTokenRequest(
            final AuthorizationCodeResourceDetails resource, final AccessTokenRequest request) {

        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        form.set("grant_type", "authorization_code");
        form.set("code", request.getAuthorizationCode());

        Object preservedState = request.getPreservedState();
        if (request.getStateKey() != null) {
            // The token endpoint has no use for the state so we don't send it back, but we are using it
            // for CSRF detection client side...
            if (preservedState == null) {
                throw new InvalidRequestException(
                        "Possible CSRF detected - state parameter was present but no state could be found");
            }
        }

        // Extracting the redirect URI from a saved request should ignore the current URI, so it's not simply a call to
        // resource.getRedirectUri()
        String redirectUri;
        // Get the redirect uri from the stored state
        if (preservedState instanceof String) {
            // Use the preserved state in preference if it is there
            // TODO: treat redirect URI as a special kind of state (this is a historical mini hack)
            redirectUri = String.valueOf(preservedState);
        } else {
            redirectUri = resource.getRedirectUri(request);
        }

        if (redirectUri != null && !"NONE".equals(redirectUri)) {
            form.set("redirect_uri", redirectUri);
        }

        return form;

    }

    private MultiValueMap<String, String> getParametersForAuthorizeRequest(GoogleAuthCodeResourceDetails resource,
            AccessTokenRequest request) {

        MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
        form.set("response_type", "code");
        form.set("client_id", resource.getClientId());

        if (request.get("scope") != null) {
            form.set("scope", request.getFirst("scope"));
        } else {
            form.set("scope", OAuth2Utils.formatParameterList(resource.getScope()));
        }

        // Extracting the redirect URI from a saved request should ignore the current URI, so it's not simply a call to
        // resource.getRedirectUri()
        String redirectUri = resource.getPreEstablishedRedirectUri();

        Object preservedState = request.getPreservedState();
        if (redirectUri == null && preservedState != null) {
            // no pre-established redirect uri: use the preserved state
            // TODO: treat redirect URI as a special kind of state (this is a historical mini hack)
            redirectUri = String.valueOf(preservedState);
        } else {
            redirectUri = request.getCurrentUri();
        }

        String stateKey = request.getStateKey();
        if (stateKey != null) {
            form.set("state", stateKey);
            if (preservedState == null) {
                throw new InvalidRequestException(
                        "Possible CSRF detected - state parameter was present but no state could be found");
            }
        }

        form.set("approval_prompt", resource.getApprovalPrompt());

        if (StringUtils.isEmpty(resource.getLoginHint())) {
            form.set("login_hint", resource.getLoginHint());
        }

        if (redirectUri != null) {
            form.set("redirect_uri", redirectUri);
        }

        return form;

    }

    private UserRedirectRequiredException getRedirectForAuthorization(GoogleAuthCodeResourceDetails resource,
            AccessTokenRequest request) {

        // we don't have an authorization code yet. So first get that.
        TreeMap<String, String> requestParameters = new TreeMap<>();
        requestParameters.put("response_type", "code"); // oauth2 spec, section 3
        requestParameters.put("client_id", resource.getClientId());
        // Client secret is not required in the initial authorization request

        String redirectUri = resource.getRedirectUri(request);
        if (redirectUri != null) {
            requestParameters.put("redirect_uri", redirectUri);
        }

        if (resource.isScoped()) {

            StringBuilder builder = new StringBuilder();
            List<String> scope = resource.getScope();

            if (scope != null) {
                Iterator<String> scopeIt = scope.iterator();
                while (scopeIt.hasNext()) {
                    builder.append(scopeIt.next());
                    if (scopeIt.hasNext()) {
                        builder.append(' ');
                    }
                }
            }

            requestParameters.put("scope", builder.toString());
        }

        requestParameters.put("approval_prompt", resource.getApprovalPrompt());

        if (StringUtils.isEmpty(resource.getLoginHint())) {
            requestParameters.put("login_hint", resource.getLoginHint());
        }

        requestParameters.put("access_type", "online");

        UserRedirectRequiredException redirectException = new UserRedirectRequiredException(
                resource.getUserAuthorizationUri(), requestParameters);

        String stateKey = stateKeyGenerator.generateKey(resource);
        redirectException.setStateKey(stateKey);
        request.setStateKey(stateKey);
        redirectException.setStateToPreserve(redirectUri);
        request.setPreservedState(redirectUri);

        return redirectException;

    }

    /**
     * Gets the content for the UserApprovalRequire exeption.
     *
     * @param resource the resource details objet
     * @param request the access toke request
     * @return the exception to be thrown
     */
    protected final UserApprovalRequiredException getUserApprovalSignal(AuthorizationCodeResourceDetails resource,
            AccessTokenRequest request) {
        String message = String.format("Do you approve the client '%s' to access your resources with scope=%s",
                resource.getClientId(), resource.getScope());
        return new UserApprovalRequiredException(resource.getUserAuthorizationUri(),
                Collections.singletonMap(OAuth2Utils.USER_OAUTH_APPROVAL, message), resource.getClientId(),
                resource.getScope());
    }

    private static class CookieResponseExtractor implements ResponseExtractor<ResponseEntity<Void>> {

        private final AccessTokenRequest copy;
        private final ResponseExtractor<ResponseEntity<Void>> delegate;

        CookieResponseExtractor(AccessTokenRequest copy, ResponseExtractor<ResponseEntity<Void>> delegate) {
            this.copy = copy;
            this.delegate = delegate;
        }

        @Override
        public ResponseEntity<Void> extractData(ClientHttpResponse response) throws IOException {
            if (response.getHeaders().containsKey("Set-Cookie")) {
                copy.setCookie(response.getHeaders().getFirst("Set-Cookie"));
            }
            return delegate.extractData(response);
        }
    }

    private static class AuthResponseExtractor implements ResponseExtractor<ResponseEntity<Void>> {

        AuthResponseExtractor() {
        }

        @Override
        public ResponseEntity<Void> extractData(ClientHttpResponse response) throws IOException {
            return new ResponseEntity<>(response.getHeaders(), response.getStatusCode());
        }
    }

}