/*
* Copyright (c) 2009 Lockheed Martin Corporation
*
* 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 org.eurekastreams.server.service.security.persistentlogin;
import java.util.Arrays;
import java.util.Date;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eurekastreams.server.domain.PersistentLogin;
import org.eurekastreams.server.service.security.userdetails.ExtendedUserDetails;
import org.springframework.security.Authentication;
import org.springframework.security.ui.rememberme.InvalidCookieException;
import org.springframework.security.ui.rememberme.TokenBasedRememberMeServices;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Persistent login service based on Spring's TokenBasedRememberMeServcies
* class. This class overrides the parents processAutoLoginCookie,
* onLoginSuccess, and logout methods to provide functionality required for
* persistent login with LDAP authentication. NOTE: This class currently
* requires a UserDetailsService implementation that returns object of type
* ExtendedUserDetails, but that can be refactored out to a strategy if there is
* ever another type of UserDetails service used with this system.
*/
public class PersistentLoginService extends TokenBasedRememberMeServices
{
/**
* Time before expiration.
*/
private static final long EXPIRE_TIME = 1000L;
/**
* The PersistentTokenRepository used by this service.
*/
private PersistentLoginRepository loginRepository = null;
/**
* Constructor.
*
* @param key
* Key used in generating token value. THIS MUST MATCH THE SAME
* VALUE PASSED TO THE SPRING FILTER IN THE "remember-me" ELEMENT
* IN THE CONTEXT FILE.
* @param userDetailsService
* The UserDetailsService this service should use.
* @param persistentLoginRepository
* PersistentTokenRepository this object uses to store login
* information
*/
public PersistentLoginService(final String key,
final UserDetailsService userDetailsService,
final PersistentLoginRepository persistentLoginRepository)
{
Assert.notNull(key);
Assert.notNull(userDetailsService);
Assert.notNull(persistentLoginRepository);
this.setKey(key);
this.setUserDetailsService(userDetailsService);
this.loginRepository = persistentLoginRepository;
}
/**
* This method overrides parents to allow persistent login using eurekastreams
* UserDetailsService and LDAP authentication.
*
* @param cookieTokens
* Provided by parent class, the decoded cookie information.
* @param request
* The request object.
* @param response
* The response object.
* @return Populated UserDetails object. In this case it's an instance of
* ExtendedUserDetails.
*/
public UserDetails processAutoLoginCookie(final String[] cookieTokens,
final HttpServletRequest request, final HttpServletResponse response)
{
//This cookie validation code section is taken straight from
//Spring's TokenBasedRememberMeServices, no need to reinvent the wheel.
if (cookieTokens.length != 3)
{
throw new InvalidCookieException("Cookie token did not contain "
+ 2 + " tokens, but contained '"
+ Arrays.asList(cookieTokens) + "'");
}
long tokenExpiryTime;
try
{
tokenExpiryTime = new Long(cookieTokens[1]).longValue();
}
catch (NumberFormatException nfe)
{
throw new InvalidCookieException(
"Cookie token[1] did not contain a valid number (contained '"
+ cookieTokens[1] + "')");
}
if (isTokenExpired(tokenExpiryTime))
{
throw new InvalidCookieException(
"Cookie token[1] has expired (expired on '"
+ new Date(tokenExpiryTime)
+ "'; current time is '" + new Date() + "')");
}
// TODO make the following validation steps a cookie validation strategy
// passing userDetails and cookieTokens
// so this class doesn't have to know about ExtendedUserDetails
// interface. Only needed if some other UserDetails service is created.
// if not expired load user details
ExtendedUserDetails userDetails = (ExtendedUserDetails)
(getUserDetailsService().loadUserByUsername(cookieTokens[0]));
//if no persistentLogin info returned from UserDetailsService, abort
//as cookie was misleading or manually invalidated.
PersistentLogin login = userDetails.getPersistentLogin();
if (login == null)
{
throw new InvalidCookieException(
"No PersistentLogin record in repository");
}
// Check signature of token matches remaining details.
// Must do this after user lookup,
String expectedTokenSignature = login.getTokenValue();
long expectedExpiryDate = login.getTokenExpirationDate();
if (tokenExpiryTime != expectedExpiryDate)
{
throw new InvalidCookieException(
"Cookie token[1] contained expirationDate '"
+ cookieTokens[2] + "' but expected '"
+ expectedExpiryDate + "'");
}
if (!expectedTokenSignature.equals(cookieTokens[2]))
{
throw new InvalidCookieException(
"Cookie token[2] contained signature '" + cookieTokens[2]
+ "' but expected '" + expectedTokenSignature + "'");
}
return userDetails;
}
/**
* Override parents version so we can generate our own custom token value
* and store persistent login information in our repository.
* @param request The request object.
* @param response The response object.
* @param successfulAuthentication The Authentication object for authenticated user.
*/
public void onLoginSuccess(final HttpServletRequest request,
final HttpServletResponse response,
final Authentication successfulAuthentication)
{
String username = retrieveUserName(successfulAuthentication);
// If unable to find a username, just abort as
// TokenBasedRememberMeServices is
// unable to construct a valid token in this case.
if (!StringUtils.hasLength(username))
{
logger.error(
"Authentication contains empty username. Unable to create persistent login info.");
return;
}
int tokenLifetime = calculateLoginLifetime(request,
successfulAuthentication);
long expiryTime = System.currentTimeMillis() + EXPIRE_TIME * tokenLifetime;
String signatureValue = makeTokenSignature(expiryTime, username,
UUID.randomUUID().toString());
// store it to repository, this is "non-essential" functionality so
// swallow any
// exceptions and log it. If for any reason this doesn't work, abort
// without
// exception and don't set cookie
try
{
PersistentLogin login = new PersistentLogin(username, signatureValue, expiryTime);
loginRepository.createOrUpdatePersistentLogin(login);
}
catch (Exception e)
{
logger.error("Unable to insert PersistentLogin information to DB for user: "
+ username
+ "', expiry: '"
+ new Date(expiryTime)
+ "'");
return;
}
setCookie(new String[] { username, Long.toString(expiryTime),
signatureValue }, tokenLifetime, request, response);
if (logger.isDebugEnabled())
{
logger.debug("Added remember-me cookie for user '" + username
+ "', expiry: '" + new Date(expiryTime) + "'");
}
}
/**
* Override parent logout method so we can remove the persistent login info
* from the repository (and cancel cookie).
*
* @param request
* The request object.
* @param response
* The response object.
* @param authentication
* The Authentication object.
*/
public void logout(final HttpServletRequest request,
final HttpServletResponse response, final Authentication authentication)
{
if (logger.isDebugEnabled())
{
logger.debug("Logout of user "
+ (authentication == null ? "Unknown" : authentication.getName()));
}
cancelCookie(request, response);
loginRepository.removePersistentLogin(((UserDetails) authentication
.getPrincipal()).getUsername());
}
}