uk.ac.cam.cl.dtg.segue.auth.SegueLocalAuthenticator.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.cam.cl.dtg.segue.auth.SegueLocalAuthenticator.java

Source

/**
 * Copyright 2014 Stephen Cummins
 *
 * 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 uk.ac.cam.cl.dtg.segue.auth;

import static uk.ac.cam.cl.dtg.segue.api.Constants.HMAC_SALT;

import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import uk.ac.cam.cl.dtg.segue.api.managers.UserAuthenticationManager;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.FailedToHashPasswordException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.IncorrectCredentialsProvidedException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.InvalidPasswordException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoCredentialsAvailableException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserException;
import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException;
import uk.ac.cam.cl.dtg.segue.dao.users.IUserDataManager;
import uk.ac.cam.cl.dtg.segue.dos.users.RegisteredUser;
import uk.ac.cam.cl.dtg.util.PropertiesLoader;

import com.google.inject.Inject;

/**
 * Segue Local Authenticator. This provides a mechanism for users to create an account on the Segue CMS without the need
 * to use a 3rd party authenticator.
 * 
 * @author Stephen Cummins
 */
public class SegueLocalAuthenticator implements IPasswordAuthenticator {
    private static final Logger log = LoggerFactory.getLogger(SegueLocalAuthenticator.class);
    private final IUserDataManager userDataManager;

    private final PropertiesLoader properties;

    private static final String CRYPTO_ALOGRITHM = "PBKDF2WithHmacSHA1";
    private static final String SALTING_ALGORITHM = "SHA1PRNG";
    private static final Integer ITERATIONS = 1000;
    private static final Integer KEY_LENGTH = 512;
    private static final Integer SHORT_KEY_LENGTH = 128;
    private static final int SALT_SIZE = 16;

    /**
     * Creates a segue local authenticator object to validate and create passwords to be stored by the Segue CMS.
     * 
     * @param userDataManager
     *            - the user data manager which allows us to store and query user information.
     * @param properties
     *            - so we can look up system properties.
     */
    @Inject
    public SegueLocalAuthenticator(final IUserDataManager userDataManager, final PropertiesLoader properties) {
        this.userDataManager = userDataManager;
        this.properties = properties;
    }

    @Override
    public AuthenticationProvider getAuthenticationProvider() {
        return AuthenticationProvider.SEGUE;
    }

    @Override
    public void setOrChangeUsersPassword(final RegisteredUser userToSetPasswordFor, final String plainTextPassword)
            throws InvalidPasswordException {
        if (null == plainTextPassword || plainTextPassword.isEmpty()) {
            throw new InvalidPasswordException("Invalid password. You cannot have an empty password.");
        }

        if (plainTextPassword.length() < 6) {
            throw new InvalidPasswordException("Password must be at least 6 characters in length.");
        }

        try {
            String passwordSalt = generateSalt();
            String hashedPassword = this.hashPassword(plainTextPassword, passwordSalt);

            userToSetPasswordFor.setPassword(hashedPassword);
            userToSetPasswordFor.setSecureSalt(passwordSalt);

        } catch (NoSuchAlgorithmException e) {
            log.error("Error detecting security algorithm", e);
            throw new FailedToHashPasswordException("Security algorithrm configuration error.");
        } catch (InvalidKeySpecException e) {
            log.error("Error building secret key specification", e);
            throw new FailedToHashPasswordException("Security algorithrm configuration error.");
        }
    }

    @Override
    public RegisteredUser authenticate(final String usersEmailAddress, final String plainTextPassword)
            throws IncorrectCredentialsProvidedException, NoUserException, NoCredentialsAvailableException,
            SegueDatabaseException {

        if (null == usersEmailAddress || null == plainTextPassword) {
            throw new IncorrectCredentialsProvidedException("Incorrect credentials provided.");
        }

        RegisteredUser localUserAccount = userDataManager.getByEmail(usersEmailAddress);

        if (null == localUserAccount) {
            throw new NoUserException();
        }
        if (null == localUserAccount.getPassword() || null == localUserAccount.getSecureSalt()) {
            log.debug("No credentials available for this account");
            throw new NoCredentialsAvailableException("This user does not have any" + " local credentials setup.");
        }

        try {
            if (this.hashPassword(plainTextPassword, localUserAccount.getSecureSalt())
                    .equals(localUserAccount.getPassword())) {
                return localUserAccount;
            } else {
                throw new IncorrectCredentialsProvidedException("Incorrect credentials provided.");
            }
        } catch (NoSuchAlgorithmException e) {
            log.error("Error detecting security algorithm", e);
            return null;
        } catch (InvalidKeySpecException e) {
            log.error("Error building secret key specification", e);
            return null;
        }
    }

    @Override
    public RegisteredUser createEmailVerificationTokenForUser(final RegisteredUser userToAttachVerificationToken,
            final String email) throws NoSuchAlgorithmException, InvalidKeySpecException {
        Validate.notNull(userToAttachVerificationToken);
        Validate.notNull(email, "Email used for verification cannot be null");

        //Get HMAC
        String key = properties.getProperty(HMAC_SALT);
        String token = UserAuthenticationManager.calculateHMAC(key, email);

        userToAttachVerificationToken
                .setEmailVerificationToken(token.replace("=", "").replace("/", "").replace("+", ""));

        return userToAttachVerificationToken;
    }

    @Override
    public boolean isValidEmailVerificationToken(final RegisteredUser user, final String email,
            final String token) {
        Validate.notNull(user);

        String userToken = user.getEmailVerificationToken();
        if (userToken != null && userToken.startsWith(token)) {
            // Check if the email corresponds to the token
            String key = properties.getProperty(HMAC_SALT);
            String hmacToken = UserAuthenticationManager.calculateHMAC(key, email).replace("=", "").replace("/", "")
                    .replace("+", "");
            return userToken.equals(hmacToken);
        }
        return false;
    }

    @Override
    public RegisteredUser createPasswordResetTokenForUser(final RegisteredUser userToAttachToken)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        Validate.notNull(userToAttachToken);
        // Trim the "=" padding off the end of the base64 encoded token so that the URL that is
        // eventually generated is correctly parsed in email clients
        String token = new String(Base64.encodeBase64(
                computeHash(UUID.randomUUID().toString(), userToAttachToken.getSecureSalt(), SHORT_KEY_LENGTH)))
                        .replace("=", "").replace("/", "").replace("+", "");

        userToAttachToken.setResetToken(token);

        // Set expiry date
        // Java is useless at datetime maths
        Calendar c = Calendar.getInstance();
        c.setTime(new Date()); // Initialises the calendar to the current date/time
        c.add(Calendar.DATE, 1);
        userToAttachToken.setResetExpiry(c.getTime());

        return userToAttachToken;
    }

    @Override
    public boolean isValidResetToken(final RegisteredUser user) {
        // Get today's datetime; this is initialised to the time at which it was allocated,
        // measured to the nearest millisecond.
        Date now = new Date();

        return user != null && user.getResetExpiry().after(now);
    }

    /**
     * Hash the password using the preconfigured hashing function.
     *
     * @param password
     *            - password to hash
     * @param salt
     *            - random string to use as a salt.
     * @return the Base64 encoded hashed password
     * @throws NoSuchAlgorithmException
     *             - if the configured algorithm is not valid.
     * @throws InvalidKeySpecException
     *             - if the preconfigured key spec is invalid.
     */
    private String hashPassword(final String password, final String salt)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        BigInteger hashedPassword = new BigInteger(computeHash(password, salt, KEY_LENGTH));

        return new String(Base64.encodeBase64(hashedPassword.toByteArray()));
    }

    /**
     * Compute the hash of a string using the preconfigured hashing function.
     *
     * @param str
     *            - string to hash
     * @param salt
     *            - random string to use as a salt.
     * @param keyLength
     *            - the key length
     * @return a byte array of the hash
     * @throws NoSuchAlgorithmException
     *             - if the configured algorithm is not valid.
     * @throws InvalidKeySpecException
     *             - if the preconfigured key spec is invalid.
     */
    private byte[] computeHash(final String str, final String salt, final int keyLength)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        char[] strChars = str.toCharArray();
        byte[] saltBytes = salt.getBytes();

        PBEKeySpec spec = new PBEKeySpec(strChars, saltBytes, ITERATIONS, keyLength);

        SecretKeyFactory key = SecretKeyFactory.getInstance(CRYPTO_ALOGRITHM);
        return key.generateSecret(spec).getEncoded();
    }

    /**
     * Helper method to generate a base64 encoded salt.
     * 
     * @return generate a base64 encoded secure salt.
     * @throws NoSuchAlgorithmException
     *             - problem locating the algorithm.
     */
    private static String generateSalt() throws NoSuchAlgorithmException {
        SecureRandom sr = SecureRandom.getInstance(SALTING_ALGORITHM);

        byte[] salt = new byte[SALT_SIZE];
        sr.nextBytes(salt);

        return new String(Base64.encodeBase64(salt));
    }

}