org.taverna.server.master.identity.StrippedDownAuthProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.taverna.server.master.identity.StrippedDownAuthProvider.java

Source

package org.taverna.server.master.identity;
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

import java.util.HashMap;
import java.util.Map;

import javax.annotation.PreDestroy;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.taverna.server.master.utils.CallTimeLogger.PerfLogged;

/**
 * A stripped down version of a
 * {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider
 * DaoAuthenticationProvider}/
 * {@link org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider
 * AbstractUserDetailsAuthenticationProvider} that avoids much of the overhead
 * associated with that class.
 */
public class StrippedDownAuthProvider implements AuthenticationProvider {
    /**
     * The plaintext password used to perform
     * {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when
     * the user is not found to avoid SEC-2056.
     */
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

    /**
     * The password used to perform
     * {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when
     * the user is not found to avoid SEC-2056. This is necessary, because some
     * {@link PasswordEncoder} implementations will short circuit if the
     * password is not in a valid format.
     */
    private String userNotFoundEncodedPassword;
    private UserDetailsService userDetailsService;
    private PasswordEncoder passwordEncoder;
    private Map<String, AuthCacheEntry> authCache = new HashMap<>();
    protected final Log logger = LogFactory.getLog(getClass());

    private static class AuthCacheEntry {
        private String creds;
        private long timestamp;
        private static final long VALIDITY = 1000 * 60 * 20;

        AuthCacheEntry(String credentials) {
            creds = credentials;
            timestamp = System.currentTimeMillis();
        }

        boolean valid(String password) {
            return creds.equals(password) && timestamp + VALIDITY > System.currentTimeMillis();
        }
    }

    @PerfLogged
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        if (!(authentication instanceof UsernamePasswordAuthenticationToken))
            throw new IllegalArgumentException("can only authenticate against username+password");
        UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;

        // Determine username
        String username = (auth.getPrincipal() == null) ? "NONE_PROVIDED" : auth.getName();

        UserDetails user;

        try {
            user = retrieveUser(username, auth);
            if (user == null)
                throw new IllegalStateException(
                        "retrieveUser returned null - a violation of the interface contract");
        } catch (UsernameNotFoundException notFound) {
            if (logger.isDebugEnabled())
                logger.debug("User '" + username + "' not found", notFound);
            throw new BadCredentialsException("Bad credentials");
        }

        // Pre-auth
        if (!user.isAccountNonLocked())
            throw new LockedException("User account is locked");
        if (!user.isEnabled())
            throw new DisabledException("User account is disabled");
        if (!user.isAccountNonExpired())
            throw new AccountExpiredException("User account has expired");
        Object credentials = auth.getCredentials();
        if (credentials == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException("Bad credentials");
        }

        String providedPassword = credentials.toString();
        boolean matched = false;
        synchronized (authCache) {
            AuthCacheEntry pw = authCache.get(username);
            if (pw != null && providedPassword != null) {
                if (pw.valid(providedPassword))
                    matched = true;
                else
                    authCache.remove(username);
            }
        }
        // Auth
        if (!matched) {
            if (!passwordEncoder.matches(providedPassword, user.getPassword())) {
                logger.debug("Authentication failed: password does not match stored value");

                throw new BadCredentialsException("Bad credentials");
            }
            if (providedPassword != null)
                synchronized (authCache) {
                    authCache.put(username, new AuthCacheEntry(providedPassword));
                }
        }

        // Post-auth
        if (!user.isCredentialsNonExpired())
            throw new CredentialsExpiredException("User credentials have expired");

        return createSuccessAuthentication(user, auth, user);
    }

    @PreDestroy
    void clearCache() {
        authCache.clear();
    }

    /**
     * Creates a successful {@link Authentication} object.
     * <p>
     * Protected so subclasses can override.
     * </p>
     * <p>
     * Subclasses will usually store the original credentials the user supplied
     * (not salted or encoded passwords) in the returned
     * <code>Authentication</code> object.
     * </p>
     * 
     * @param principal
     *            that should be the principal in the returned object (defined
     *            by the {@link #isForcePrincipalAsString()} method)
     * @param authentication
     *            that was presented to the provider for validation
     * @param user
     *            that was loaded by the implementation
     * 
     * @return the successful authentication token
     */
    private Authentication createSuccessAuthentication(Object principal, Authentication authentication,
            UserDetails user) {
        /*
         * Ensure we return the original credentials the user supplied, so
         * subsequent attempts are successful even with encoded passwords. Also
         * ensure we return the original getDetails(), so that future
         * authentication events after cache expiry contain the details
         */
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
                authentication.getCredentials(), user.getAuthorities());
        result.setDetails(authentication.getDetails());

        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

    /**
     * Allows subclasses to actually retrieve the <code>UserDetails</code> from
     * an implementation-specific location, with the option of throwing an
     * <code>AuthenticationException</code> immediately if the presented
     * credentials are incorrect (this is especially useful if it is necessary
     * to bind to a resource as the user in order to obtain or generate a
     * <code>UserDetails</code>).
     * <p>
     * Subclasses are not required to perform any caching, as the
     * <code>AbstractUserDetailsAuthenticationProvider</code> will by default
     * cache the <code>UserDetails</code>. The caching of
     * <code>UserDetails</code> does present additional complexity as this means
     * subsequent requests that rely on the cache will need to still have their
     * credentials validated, even if the correctness of credentials was assured
     * by subclasses adopting a binding-based strategy in this method.
     * Accordingly it is important that subclasses either disable caching (if
     * they want to ensure that this method is the only method that is capable
     * of authenticating a request, as no <code>UserDetails</code> will ever be
     * cached) or ensure subclasses implement
     * {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)}
     * to compare the credentials of a cached <code>UserDetails</code> with
     * subsequent authentication requests.
     * </p>
     * <p>
     * Most of the time subclasses will not perform credentials inspection in
     * this method, instead performing it in
     * {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)}
     * so that code related to credentials validation need not be duplicated
     * across two methods.
     * </p>
     * 
     * @param username
     *            The username to retrieve
     * @param authentication
     *            The authentication request, which subclasses <em>may</em> need
     *            to perform a binding-based retrieval of the
     *            <code>UserDetails</code>
     * 
     * @return the user information (never <code>null</code> - instead an
     *         exception should the thrown)
     * 
     * @throws AuthenticationException
     *             if the credentials could not be validated (generally a
     *             <code>BadCredentialsException</code>, an
     *             <code>AuthenticationServiceException</code> or
     *             <code>UsernameNotFoundException</code>)
     */
    private UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        try {
            return userDetailsService.loadUserByUsername(username);
        } catch (UsernameNotFoundException notFound) {
            if (authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
            }
            throw notFound;
        } catch (AuthenticationException e) {
            throw e;
        } catch (Exception repositoryProblem) {
            throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
        }
    }

    /**
     * Sets the PasswordEncoder instance to be used to encode and validate
     * passwords.
     */
    @Required
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        if (passwordEncoder == null)
            throw new IllegalArgumentException("passwordEncoder cannot be null");

        this.passwordEncoder = passwordEncoder;
        this.userNotFoundEncodedPassword = passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
    }

    @Required
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        if (userDetailsService == null)
            throw new IllegalStateException("A UserDetailsService must be set");
        this.userDetailsService = userDetailsService;
    }
}