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

net.smartcosmos.extension.stormpath.service.UserDetailsServiceStormpath Maven / Gradle / Ivy

The newest version!
package net.smartcosmos.extension.stormpath.service;

import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;

import com.stormpath.sdk.account.Account;
import com.stormpath.sdk.account.AccountCriteria;
import com.stormpath.sdk.account.AccountList;
import com.stormpath.sdk.account.AccountStatus;
import com.stormpath.sdk.account.Accounts;
import com.stormpath.sdk.application.Application;
import com.stormpath.sdk.authc.AuthenticationRequest;
import com.stormpath.sdk.authc.AuthenticationResult;
import com.stormpath.sdk.authc.UsernamePasswordRequests;
import com.stormpath.sdk.client.Client;
import com.stormpath.sdk.directory.DirectoryStatus;
import com.stormpath.sdk.error.Error;
import com.stormpath.sdk.lang.Assert;
import com.stormpath.sdk.resource.ResourceException;
import com.stormpath.sdk.tenant.Tenant;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import net.smartcosmos.extension.stormpath.config.StormpathProperties;
import net.smartcosmos.userdetails.domain.UserDetails;
import net.smartcosmos.userdetails.service.UserDetailsService;

import static com.stormpath.sdk.account.Accounts.where;

import static net.smartcosmos.extension.stormpath.service.StormpathErrorCodeConstants.ERR_ACCOUNT_DISABLED;
import static net.smartcosmos.extension.stormpath.service.StormpathErrorCodeConstants.ERR_ACCOUNT_LOCKED;
import static net.smartcosmos.extension.stormpath.service.StormpathErrorCodeConstants.ERR_ACCOUNT_NONEXISTENT;
import static net.smartcosmos.extension.stormpath.service.StormpathErrorCodeConstants.ERR_ACCOUNT_STORE_DISABLED;
import static net.smartcosmos.extension.stormpath.service.StormpathErrorCodeConstants.ERR_ACCOUNT_UNVERIFIED;
import static net.smartcosmos.extension.stormpath.service.StormpathErrorCodeConstants.ERR_DIRECTORY_DISABLED;
import static net.smartcosmos.extension.stormpath.service.StormpathErrorCodeConstants.ERR_GROUP_DISABLED;
import static net.smartcosmos.extension.stormpath.service.StormpathErrorCodeConstants.ERR_ORGANIZATION_DISABLED;
import static net.smartcosmos.extension.stormpath.service.StormpathErrorCodeConstants.ERR_PASSWORD_INCORRECT;
import static net.smartcosmos.extension.stormpath.util.StormpathInitializer.getClientFromProperties;
import static net.smartcosmos.extension.stormpath.util.StormpathInitializer.getSingleApplication;

/**
 * Stormpath-specific implementation of the {@link UserDetailsService} interface.
 */
@Slf4j
@Service
public class UserDetailsServiceStormpath implements UserDetailsService {

    private Application application = null;

    private final ConversionService conversionService;
    private final PasswordEncoder passwordEncoder;
    private final StormpathProperties stormpathProperties;
    private final Validator validator;

    @Autowired
    public UserDetailsServiceStormpath(
        StormpathProperties stormpathProperties,
        ConversionService conversionService,
        PasswordEncoder passwordEncoder,
        Validator validator) {

        this.conversionService = conversionService;
        this.passwordEncoder = passwordEncoder;
        this.stormpathProperties = stormpathProperties;
        this.validator = validator;

        initialize();
    }

    @Override
    public UserDetails getUserDetails(String username, String password) throws IllegalArgumentException, AuthenticationException {

        Assert.isTrue(StringUtils.isNotBlank(username), "username may not be blank");
        Assert.isTrue(StringUtils.isNotBlank(password), "password may not be blank");

        if (isInitialized()) {
            log.debug("Stormpath is attempting to authenticate user {}...", username);

            AuthenticationRequest request = UsernamePasswordRequests.builder()
                .setUsernameOrEmail(username)
                .setPassword(password)
                .build();

            try {
                AuthenticationResult result = application.authenticateAccount(request);
                Account account = result.getAccount();

                log.info("Stormpath has successfully authenticated user {}", username);
                log.debug("Details for account {}: {}", username, account);

                UserDetails userResponse = conversionService.convert(account, UserDetails.class);
                userResponse.setPasswordHash(passwordEncoder.encode(password));

                return userResponse;

            } catch (ResourceException e) {
                String message = String.format("Stormpath authentication request or user %s failed: %s",
                                               username,
                                               e.toString());
                log.info(message);
                log.debug(message, e);

                throwAuthenticationExceptionForError(message, e);
            }
        }
        log.error("Stormpath application is not initialized");
        throw new InternalAuthenticationServiceException("Stormpath application could not be initialized");
    }

    @Override
    public UserDetails getUserDetails(String username) throws IllegalArgumentException, AuthenticationException {

        Assert.isTrue(StringUtils.isNotBlank(username), "username may not be blank");

        if (isInitialized()) {

            AccountCriteria accountCriteria = // @formatter:off
                where(Accounts.email().eqIgnoreCase(username));
            // @formatter:on
            AccountList accountList = application.getAccounts(accountCriteria);

            if (accountList == null || accountList.getSize() == 0) {
                String msg = String.format("Unable to locate username '%s'", username);
                log.info(msg);
                throw new UsernameNotFoundException(msg);
            }

            if (accountList.getSize() > 1) {
                String msg = String.format("No unique result for username '%s'", username);
                log.info(msg);
                log.debug("Stormpath returned account list {}", accountList);
                throw new AuthenticationServiceException(msg);
            }

            Account account = accountList.single();

            if (!AccountStatus.ENABLED.equals(account.getStatus()) ||
                !DirectoryStatus.ENABLED.equals(account.getDirectory()
                                                    .getStatus())) {
                String msg = String.format("Account or directory for username '%s' is disabled", username);
                log.info(msg);
                throw new DisabledException(msg);
            }

            log.info("Stormpath has successfully returned details for user {}", username);
            log.debug("Details for account {}: {}", username, account);
            return conversionService.convert(account, UserDetails.class);
        }
        log.error("Stormpath application is not initialized");
        throw new InternalAuthenticationServiceException("Stormpath application could not be initialized");
    }

    @Override
    public boolean isValid(UserDetails userDetails) {

        log.debug("Entity: {}", userDetails);
        Set> violations = validator.validate(userDetails);
        log.debug("Constraint violations: {}", violations.toString());

        return violations.isEmpty();
    }

    public boolean initialize() {

        try {
            String applicationName = stormpathProperties.getApplicationName();
            Client stormpathClient = getClientFromProperties(stormpathProperties.getApiKey());
            Tenant tenant = stormpathClient.getCurrentTenant();

            application = getSingleApplication(tenant, applicationName);

            log.info("Stormpath has successfully initialized application {}", applicationName);
            log.debug("Details for application {}: {}", applicationName, application);
        } catch (NullPointerException e) {
            log.warn(
                "Initialization of Stormpath service failed. This generally indicates that the Stormpath API key and/or applicationName is missing "
                + "in the configuration: {}",
                e);
        }
        return application != null;
    }

    private boolean isInitialized() {

        if (application == null) {
            log.info("Application is not initialized. Attempting again...");

            if (!initialize()) {
                log.error("Initializing failed again. Authentication attempt is aborted.");
            }
        }
        return application != null;
    }

    private void throwAuthenticationExceptionForError(String message, ResourceException e) throws AuthenticationException {

        Error error = e.getStormpathError();
        log.info("Stormpath returned error: {}", error);

        switch (error.getCode()) {
            case ERR_ACCOUNT_NONEXISTENT:
                throw new UsernameNotFoundException(message, e);
            case ERR_PASSWORD_INCORRECT:
                throw new BadCredentialsException(message, e);
            case ERR_ACCOUNT_LOCKED:
                throw new LockedException(message, e);
            case ERR_ACCOUNT_DISABLED:
            case ERR_ACCOUNT_UNVERIFIED:
            case ERR_GROUP_DISABLED:
            case ERR_DIRECTORY_DISABLED:
            case ERR_ORGANIZATION_DISABLED:
            case ERR_ACCOUNT_STORE_DISABLED:
                throw new DisabledException(message, e);
            default:
                // If none of the above error codes applied, this is likely to be a more general service problem
                throw new AuthenticationServiceException(message, e);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy