package org.craftercms.security.authentication.impl; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.craftercms.commons.crypto.CryptoException; import org.craftercms.commons.crypto.TextEncryptor; import org.craftercms.commons.http.CookieManager; import org.craftercms.commons.http.HttpUtils; import org.craftercms.commons.http.RequestContext; import org.craftercms.profile.api.PersistentLogin; import org.craftercms.profile.api.Profile; import org.craftercms.profile.api.exceptions.ProfileException; import org.craftercms.profile.api.services.AuthenticationService; import org.craftercms.profile.api.services.ProfileService; import org.craftercms.security.authentication.Authentication; import org.craftercms.security.authentication.AuthenticationManager; import org.craftercms.security.authentication.RememberMeManager; import org.craftercms.security.exception.AuthenticationException; import org.craftercms.security.exception.AuthenticationSystemException; import org.craftercms.security.exception.rememberme.CookieTheftException; import org.craftercms.security.exception.rememberme.InvalidCookieException; import org.craftercms.security.exception.rememberme.RememberMeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Required; /** * Default implementation of {@link org.craftercms.security.authentication.RememberMeManager}. * * @author avasquez */ public class RememberMeManagerImpl implements RememberMeManager { private static final Logger logger = LoggerFactory.getLogger(AuthenticationManagerImpl.class); public static final String REMEMBER_ME_COOKIE_NAME = "remember-me"; public static final char SERIALIZED_LOGIN_SEPARATOR = ':'; protected AuthenticationService authenticationService; protected AuthenticationManager authenticationManager; protected ProfileService profileService; protected TextEncryptor encryptor; protected CookieManager rememberMeCookieManager; @Required public void setAuthenticationService(final AuthenticationService authenticationService) { this.authenticationService = authenticationService; } @Required public void setAuthenticationManager(final AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Required public void setProfileService(final ProfileService profileService) { this.profileService = profileService; } @Required public void setEncryptor(final TextEncryptor encryptor) { this.encryptor = encryptor; } @Required public void setRememberMeCookieManager(final CookieManager rememberMeCookieManager) { this.rememberMeCookieManager = rememberMeCookieManager; } @Override public Authentication autoLogin(RequestContext context) throws RememberMeException { PersistentLogin login = getPersistentLoginFromCookie(context.getRequest()); if (login != null) { PersistentLogin actualLogin; try { actualLogin = authenticationService.getPersistentLogin(login.getId()); } catch (ProfileException e) { throw new RememberMeException("Error retrieving persistent login '" + login.getProfileId() + "'"); } if (actualLogin != null) { if (!login.getProfileId().equals(actualLogin.getProfileId())) { throw new InvalidCookieException("Profile ID mismatch"); } else if (!login.getToken().equals(actualLogin.getToken())) { throw new CookieTheftException("Token mismatch. Implies a cookie theft"); } else { String loginId = actualLogin.getId(); String profileId = actualLogin.getProfileId(); logger.debug("Remember me cookie match for {}. Starting auto-login", actualLogin); Authentication auth; try { auth = authenticate(profileId); } catch (AuthenticationException e) { // Delete remember me cookie so that we don't retry auto login in next request disableRememberMe(loginId, context); throw new RememberMeException("Unable to auto-login user '" + profileId + "'", e); } updateRememberMe(loginId, context); return auth; } } else { logger.debug("No persistent login found for ID '{}' (has possibly expired)", login.getId()); deleteRememberMeCookie(context.getResponse()); return null; } } else { return null; } } @Override public void enableRememberMe(Authentication authentication, RequestContext context) throws RememberMeException { String profileId = authentication.getProfile().getId().toString(); PersistentLogin login; try { login = authenticationService.createPersistentLogin(profileId); } catch (ProfileException e) { throw new RememberMeException("Error creating persistent login for profile '" + profileId + "'", e); } logger.debug("Persistent login created: {}", login); addRememberMeCookie(serializeLogin(login), context.getResponse()); } @Override public void disableRememberMe(RequestContext context) throws RememberMeException { PersistentLogin login = getPersistentLoginFromCookie(context.getRequest()); if (login != null) { disableRememberMe(login.getId(), context); } } protected void disableRememberMe(String loginId, RequestContext context) throws RememberMeException { deleteRememberMeCookie(context.getResponse()); try { authenticationService.deletePersistentLogin(loginId); } catch (ProfileException e) { throw new RememberMeException("Error invalidating persistent login '" + loginId + "'"); } logger.debug("Persistent login '{}' invalidated", loginId); } protected void updateRememberMe(String loginId, RequestContext context) throws RememberMeException { PersistentLogin login; try { login = authenticationService.refreshPersistentLoginToken(loginId); } catch (ProfileException e) { throw new RememberMeException("Unable to update persistent login '" + loginId + "'", e); } logger.debug("Persistent login updated: {}", login); addRememberMeCookie(serializeLogin(login), context.getResponse()); } protected String serializeLogin(PersistentLogin login) throws RememberMeException { StringBuilder serializedLogin = new StringBuilder(); serializedLogin.append(login.getId()).append(SERIALIZED_LOGIN_SEPARATOR); serializedLogin.append(login.getProfileId()).append(SERIALIZED_LOGIN_SEPARATOR); serializedLogin.append(login.getToken()); try { return encryptor.encrypt(serializedLogin.toString()); } catch (CryptoException e) { throw new RememberMeException("Unable to encrypt remember me cookie", e); } } protected PersistentLogin deserializeLogin(String serializedLogin) throws RememberMeException { String decryptedLogin; try { decryptedLogin = encryptor.decrypt(serializedLogin); } catch (CryptoException e) { throw new RememberMeException("Unable to decrypt remember me cookie", e); } String[] splitSerializedLogin = StringUtils.split(decryptedLogin, SERIALIZED_LOGIN_SEPARATOR); if (ArrayUtils.isNotEmpty(splitSerializedLogin) && splitSerializedLogin.length == 3) { PersistentLogin login = new PersistentLogin(); login.setId(splitSerializedLogin[0]); login.setProfileId(splitSerializedLogin[1]); login.setToken(splitSerializedLogin[2]); return login; } else { throw new InvalidCookieException("Invalid format of remember me cookie"); } } protected void addRememberMeCookie(String cookieValue, HttpServletResponse response) { rememberMeCookieManager.addCookie(REMEMBER_ME_COOKIE_NAME, cookieValue, response); } protected String getRememberMeCookie(HttpServletRequest request) { return HttpUtils.getCookieValue(REMEMBER_ME_COOKIE_NAME, request); } protected void deleteRememberMeCookie(HttpServletResponse response) { rememberMeCookieManager.deleteCookie(REMEMBER_ME_COOKIE_NAME, response); } protected PersistentLogin getPersistentLoginFromCookie(HttpServletRequest request) { String cookie = getRememberMeCookie(request); if (StringUtils.isNotEmpty(cookie)) { return deserializeLogin(cookie); } else { return null; } } protected Authentication authenticate(String profileId) throws AuthenticationException { Profile profile; try { profile = profileService.getProfile(profileId); } catch (ProfileException e) { throw new AuthenticationSystemException("Error retrieving profile '" + profileId + "'", e); } if (profile != null) { return authenticationManager.authenticateUser(profile, true); } else { throw new AuthenticationSystemException("No profile found for ID '" + profileId + "'"); } } }