All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.racquettrack.security.oauth.OAuth2AuthenticationProvider Maven / Gradle / Ivy

The newest version!
package com.racquettrack.security.oauth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.util.Assert;

import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.Map;

/**
 * Processes an OAuth2 authentication request. The request will typically originate from a
 * {@link OAuth2AuthenticationFilter} and will operate on a {@link OAuth2AuthenticationToken}.
 *
 * The OAuth2 processes falls somewhere in between Spring Security's Authenticated and PreAuthenticated models. The
 * Authenticated model is used as we still need to exchange the OAuth code in order to get a OAuth token.
 *
 * For that reason the implementation bears similarities to
 * {@link org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider} in
 * addition to {@link org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider},
 * particularly the implementation of the {@link #authenticate(org.springframework.security.core.Authentication)}
 * method.
 *
 * Once the token is obtained, the
 * AuthenticationUserDetailsService implementation may still throw a UsernameNotFoundException, for example.
 *
 * @author paul.wheeler
 */
public class OAuth2AuthenticationProvider implements AuthenticationProvider, InitializingBean {
    private static final Logger LOGGER = LoggerFactory.getLogger(OAuth2AuthenticationProvider.class);

    private AuthenticationUserDetailsService authenticatedUserDetailsService = null;
    private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
    private boolean throwExceptionWhenTokenRejected = false;
    private OAuth2ServiceProperties oAuth2ServiceProperties = null;
    private Client client = null;

    /**
     * Check whether all required properties have been set.
     */
    public void afterPropertiesSet() {
        Assert.notNull(authenticatedUserDetailsService, "An AuthenticationUserDetailsService must be set");
        Assert.notNull(oAuth2ServiceProperties, "An oAuth2ServiceProperties must be set");
    }

    /**
     * Performs authentication with the same contract as {@link
     * org.springframework.security.authentication.AuthenticationManager#authenticate(org.springframework.security.core.Authentication)}.
     *
     * @param authentication the authentication request object.
     * @return a fully authenticated object including credentials. May return null if the
     *         AuthenticationProvider is unable to support authentication of the passed
     *         Authentication object. In such a case, the next AuthenticationProvider that
     *         supports the presented Authentication class will be tried.
     * @throws org.springframework.security.core.AuthenticationException
     *          if authentication fails.
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        if (!supports(authentication.getClass())) {
            return null;
        }

        LOGGER.debug("OAuth2Authentication authentication request: " + authentication);

        if (authentication.getCredentials() == null) {
            LOGGER.debug("No credentials found in request.");

            if (throwExceptionWhenTokenRejected) {
                throw new BadCredentialsException("No pre-authenticated credentials found in request.");
            }
            return null;
        }

        String token = getAccessToken(authentication);

        OAuth2AuthenticationToken tmpToken = new OAuth2AuthenticationToken(token);

        UserDetails ud = authenticatedUserDetailsService.loadUserDetails(tmpToken);

        userDetailsChecker.check(ud);

        OAuth2AuthenticationToken result =
                new OAuth2AuthenticationToken(ud, token, ud.getAuthorities());
        result.setDetails(authentication.getDetails());

        return result;
    }

    /**
     * Indicate that this provider only supports {@link OAuth2AuthenticationToken} (sub)classes.
     *
     * @param authentication The authentication object presented.
     * @return true if the implementation can more closely evaluate the Authentication class
     *         presented
     */
    @Override
    public boolean supports(Class authentication) {
        return OAuth2AuthenticationToken.class.isAssignableFrom(authentication);
    }

    /**
     * Set the AuthenticatedUserDetailsService to be used to load the {@code UserDetails} for the authenticated user.
     *
     * @param uds The {@link AuthenticationUserDetailsService} to use.
     */
    public void setAuthenticatedUserDetailsService(AuthenticationUserDetailsService uds) {
        this.authenticatedUserDetailsService = uds;
    }

    /**
     * If true, causes the provider to throw a BadCredentialsException if the presented authentication
     * request is invalid (contains a null principal or credentials). Otherwise it will just return
     * null. Defaults to false.
     * @param throwExceptionWhenTokenRejected True to throw an exception if the token is rejected.
     */
    public void setThrowExceptionWhenTokenRejected(boolean throwExceptionWhenTokenRejected) {
        this.throwExceptionWhenTokenRejected = throwExceptionWhenTokenRejected;
    }

    /**
     * Sets the strategy which will be used to validate the loaded UserDetails object
     * for the user. Defaults to an {@link org.springframework.security.authentication.AccountStatusUserDetailsChecker}.
     * @param userDetailsChecker The {@link UserDetailsChecker} to use.
     */
    public void setUserDetailsChecker(UserDetailsChecker userDetailsChecker) {
        Assert.notNull(userDetailsChecker, "userDetailsChacker cannot be null");
        this.userDetailsChecker = userDetailsChecker;
    }

    /**
     * Exchange the current {@link Authentication}, which should be an instance of {@link OAuth2AuthenticationToken}
     * containing an OAuth2 code as the credential, for an OAuth2 token.
     * @param authentication Expected to be an instance of a {@link OAuth2AuthenticationToken}.
     * @return The OAuth2 token from the OAuth Provider.
     */
    protected String getAccessToken(Authentication authentication) {
        String accessToken;

        try {
            Response response = getResponseForAccessTokenRequestFrom(authentication);

            if (!isOkay(response)) {
                throw new AuthenticationServiceException("Got HTTP error code from OAuth2 provider: "
                        + response.getStatus());
            }

            Map userData = getUserDataMapFrom(response);
            // Check to see if there was an error or not
            if (userData.containsKey("error")) {
                String output = response.readEntity(String.class);
                LOGGER.error("Got error response from the OAuth Provider, output={}",
                        output);
                throw new AuthenticationServiceException("Credentials were rejected by the OAuth Provider, output=" + output);
            }

            accessToken = (String)userData.get(oAuth2ServiceProperties.getAccessTokenName());

        } catch (WebApplicationException | ProcessingException e) {
            LOGGER.error("Error thrown by Jersey client when exchanging code for token", e);
            throw new AuthenticationServiceException("Error thrown by Jersey client when exchanging code for token", e);
        }

        return accessToken;
    }

    private Response getResponseForAccessTokenRequestFrom(Authentication authentication) {
        Client client = getClient();

        MultivaluedMap values = new MultivaluedHashMap<>();
        values.add(oAuth2ServiceProperties.getGrantTypeParamName(), oAuth2ServiceProperties.getGrantType());
        values.add(oAuth2ServiceProperties.getClientIdParamName(), oAuth2ServiceProperties.getClientId());
        values.add(oAuth2ServiceProperties.getClientSecretParamName(), oAuth2ServiceProperties.getClientSecret());
        values.add(oAuth2ServiceProperties.getCodeParamName(), (String)  authentication.getCredentials());
        URI redirectUri = redirectUriUsing(authentication);
        values.add(oAuth2ServiceProperties.getRedirectUriParamName(), redirectUri.toString());

        WebTarget webTarget = client.target(oAuth2ServiceProperties.getAccessTokenUri());
        return webTarget
                .request(MediaType.APPLICATION_JSON_TYPE)
                .post(Entity.form(values));
    }

    private boolean isOkay(Response response) {
        return response != null && response.getStatusInfo() == Response.Status.OK;
    }

    private Map getUserDataMapFrom(Response response) throws AuthenticationServiceException {
        return response.readEntity(new GenericType>() {});
    }

    /**
     * If a dynamic scheme, host, port, and context path was set then use it to generate the redirect URI.
     * Uses the details on the {@link OAuth2WebAuthenticationDetails} combined with
     * {@link OAuth2ServiceProperties#getRedirectUri()}.
     * @param authentication    The {@link Authentication} token.
     * @return  The dynamic redirect URI, or {@code null} if one could not be obtained.
     */
    private URI redirectUriUsing(Authentication authentication) {
        URI redirectUri;

        Object details = authentication.getDetails();
        if (details != null && OAuth2WebAuthenticationDetails.class.isAssignableFrom(details.getClass())
                && !oAuth2ServiceProperties.getRedirectUri().isAbsolute()) {
            OAuth2WebAuthenticationDetails oAuth2WebAuthenticationDetails = (OAuth2WebAuthenticationDetails) details;
            redirectUri = UriBuilder.fromPath(oAuth2WebAuthenticationDetails.getContextPath())
                    .path(oAuth2ServiceProperties.getRedirectUri().toString())
                    .scheme(oAuth2WebAuthenticationDetails.getScheme())
                    .host(oAuth2WebAuthenticationDetails.getHost())
                    .port(oAuth2WebAuthenticationDetails.getPort())
                    .build();
        } else {
            redirectUri = oAuth2ServiceProperties.getRedirectUri();
        }

        return redirectUri;
    }

    public void setoAuth2ServiceProperties(OAuth2ServiceProperties oAuth2ServiceProperties) {
        this.oAuth2ServiceProperties = oAuth2ServiceProperties;
    }

    /**
     * For caching the {@link Client} object.
     * @return The Jersey {@link Client} object to use.
     */
    public Client getClient() {
        if (client == null) {
            client = ClientBuilder.newClient();
        }
        return client;
    }

    /**
     * Intended to be used for unit testing only.
     * @param client The {@link Client} to use. For unit tests allows the client to be mocked.
     */
    public void setClient(Client client) {
        this.client = client;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy