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

net.smartcosmos.cluster.auth.SmartCosmosAuthenticationProvider Maven / Gradle / Ivy

Go to download

SMART COSMOS Authorization Server handles authentication throughout the microservice fleet

The newest version!
package net.smartcosmos.cluster.auth;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.json.JacksonJsonParser;
import org.springframework.boot.json.JsonParser;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import net.smartcosmos.cluster.auth.domain.UserResponse;
import net.smartcosmos.security.SecurityResourceProperties;
import net.smartcosmos.security.user.SmartCosmosCachedUser;

import static org.apache.commons.lang.StringUtils.defaultIfBlank;
import static org.apache.commons.lang.StringUtils.isNotBlank;

@Slf4j
@Service
@Profile("!test")
@EnableConfigurationProperties({ SecurityResourceProperties.class })
public class SmartCosmosAuthenticationProvider
    extends AbstractUserDetailsAuthenticationProvider implements UserDetailsService {

    public static final int MILLISECS_PER_SEC = 1000;
    private final PasswordEncoder passwordEncoder;
    private final Map users = new HashMap<>();
    private String userDetailsServerLocationUri;
    private RestTemplate restTemplate;
    private Integer cachedUserKeepAliveSecs;

    @Autowired
    public SmartCosmosAuthenticationProvider(
        SecurityResourceProperties securityResourceProperties,
        PasswordEncoder passwordEncoder,
        @Qualifier("userDetailsRestTemplate") RestTemplate restTemplate) {

        this.passwordEncoder = passwordEncoder;
        this.restTemplate = restTemplate;
        this.cachedUserKeepAliveSecs = securityResourceProperties.getCachedUserKeepAliveSecs();

        this.userDetailsServerLocationUri = securityResourceProperties.getUserDetails()
            .getServer()
            .getLocationUri();
    }

    /**
     * This is where the password is actually checked for caching purposes. Assuming the
     * same password encoder was used on both the user details service and here, this will
     * avoid another round trip for authentication.
     *
     * @param userDetails the recently retrieved or previously cached details.
     * @param authentication the presented token for authentication
     * @throws AuthenticationException failure to authenticate.
     */
    @Override
    protected void additionalAuthenticationChecks(
        UserDetails userDetails,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {

        String username = userDetails.getUsername() != null ? userDetails.getUsername() : "(NULL)";

        if (authentication.getCredentials() == null) {
            log.debug("Authentication failed for user {}: no credentials provided", username);

            throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
        }

        String presentedPassword = authentication.getCredentials()
            .toString();

        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            log.debug("Authentication failed for user {}: password does not match stored value", username);

            throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
        }
    }

    protected UserResponse fetchUser(String username, UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException, OAuth2Exception {

        try {
            if (authentication != null) {
                // Authenticating the user.
                UserResponse response = restTemplate.exchange(userDetailsServerLocationUri + "/authenticate",
                                                              HttpMethod.POST, new HttpEntity(authentication),
                                                              UserResponse.class, username)
                    .getBody();

                // this should not increase the log output too much, because user details will be only fetched on a cache miss
                log.debug("Fetching details for user {} with authentication token {} succeeded: {}", username, authentication, response);
                return response;
            } else {
                // Checking to see if user is still active
                URI activeUri = UriComponentsBuilder.fromUriString(userDetailsServerLocationUri)
                    .pathSegment("active")
                    .pathSegment(username)
                    .build()
                    .toUri();
                UserResponse response = restTemplate.exchange(activeUri,
                                                              HttpMethod.GET, HttpEntity.EMPTY,
                                                              UserResponse.class)
                    .getBody();

                // this should not increase the log output too much, because user details will be only fetched on a cache miss
                log.debug("Fetching details for user {} during refresh token succeeded: {}", username, response);
                return response;
            }

        } catch (HttpStatusCodeException e) {
            log.debug("Fetching details for user {} with authentication token {} failed: {} - {}",
                      username,
                      authentication,
                      e.toString(),
                      e.getResponseBodyAsString());
            switch (e.getStatusCode()) {
                case UNAUTHORIZED:
                    log.warn(
                        "Auth Server or User Details Service not properly configured to use SMART COSMOS Security Credentials; all requests will "
                        + "fail.");
                    // creates a server_error response further on
                    throw new IllegalStateException("Service not properly configured to use credentials", e);
                case BAD_REQUEST:
                    String responseMessage = getErrorResponseMessage(e);
                    if (!StringUtils.isEmpty(responseMessage)) {
                        // creates an invalid_grant OAuthException further on
                        // see org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter
                        // also see https://tools.ietf.org/html/rfc6749#section-5.2
                        throw new BadCredentialsException(responseMessage, e);
                    }
                default:
                    // creates a server_error response further on
                    // see https://tools.ietf.org/html/rfc6749#section-4.1.2.1
                    throw new RuntimeException(defaultIfBlank(getErrorResponseMessage(e), e.getMessage()), e);
            }
        } catch (Exception e) {
            log.debug("Fetching details for user {} with authentication token {} failed: {}", username, authentication, e);
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    @Override
    protected UserDetails retrieveUser(
        String username,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {

        log.debug("Authenticating, {}", username);

        SmartCosmosCachedUser cachedUser = checkedCachedUser(username);
        if (cachedUser != null) {
            if (!StringUtils.isEmpty(authentication.getCredentials())
                && !StringUtils.isEmpty(cachedUser.getPassword())) {
                if (passwordEncoder.matches(
                    authentication.getCredentials()
                        .toString(),
                    cachedUser.getPassword())) {
                    log.debug("Retrieved user {} from auth server cache.", cachedUser.getUsername());
                    return cachedUser;
                }
            }
        }

        UserResponse userResponse = fetchUser(username, authentication);

        log.trace("Received response of: {}", userResponse);

        final SmartCosmosCachedUser user = new SmartCosmosCachedUser(
            userResponse.getTenantUrn(),
            userResponse.getUserUrn(),
            userResponse.getUsername(),
            userResponse.getPasswordHash(),
            userResponse.getAuthorities()
                .stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet()));

        users.put(userResponse.getUsername(), user);

        log.debug("Retrieved user {} from user details service.", userResponse.getUsername());
        return user;
    }

    private String getErrorResponseMessage(HttpStatusCodeException exception) {

        MediaType contentType = exception.getResponseHeaders()
            .getContentType();
        if (MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)) {
            JsonParser jsonParser = new JacksonJsonParser();
            Map responseBody = jsonParser.parseMap(exception.getResponseBodyAsString());
            if (responseBody.containsKey("message") && responseBody.get("message") instanceof String) {
                return (String) responseBody.get("message");
            }
        }
        return "";
    }

    private SmartCosmosCachedUser checkedCachedUser(String username) {

        if (users.containsKey(username)) {
            final SmartCosmosCachedUser cachedUser = users.get(username);

            if (System.currentTimeMillis() - cachedUser.getCachedDate()
                .getTime() > cachedUserKeepAliveSecs * MILLISECS_PER_SEC) {
                users.remove(username);
            } else {
                return cachedUser;
            }
        }
        return null;
    }

    /**
     * This method is only utilized during the REFRESH Token phase and is designed only to see if the user account is still active.  No password is
     * provided, because there was no password used by the client -- only the refresh token.
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        log.debug("Checking to see if account {} is still active", username);

        //        SmartCosmosCachedUser user = checkedCachedUser(username);
        //        if (user != null) {
        //            return user;
        //        }

        UserResponse userResponse = fetchUser(username, null);

        log.trace("Received response of: {}", userResponse);

        log.debug("Retrieved user {} from user details service.", userResponse.getUsername());

        final SmartCosmosCachedUser user = new SmartCosmosCachedUser(
            userResponse.getTenantUrn(),
            userResponse.getUserUrn(),
            userResponse.getUsername(),
            getPasswordHash(userResponse),
            userResponse.getAuthorities()
                .stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toSet()));

        users.put(userResponse.getUsername(), user);

        return user;
    }

    /**
     * 

Gets a password hash for the returned user.

*

The method checks if the User contained in the {@link UserResponse} is present in the cache, and returns the cached password hash.

*

The response of this method must not be {@code null}, otherwise an exception will be thrown when attempting to instantiate * {@link SmartCosmosCachedUser}.

* * @param userResponse the User response from the User Details Service * @return the password hash from the cached user, or an empty String if absent */ private String getPasswordHash(UserResponse userResponse) { SmartCosmosCachedUser cachedUser = checkedCachedUser(userResponse.getUsername()); if (cachedUser != null && isNotBlank(cachedUser.getPassword()) && cachedUser.getAccountUrn() .equals(userResponse.getTenantUrn()) && cachedUser.getUserUrn() .equals(userResponse.getUserUrn())) { return cachedUser.getPassword(); } return ""; } }