I ended up creating a custom DaoAuthenticationProvider and overriding the authenticate() method. This works well enough but I would have preferred a more elegant solution.
Changed
Code:
if (!user.isCredentialsNonExpired()) {
logger.warn(user.getUsername() + " credentials expired");
throw new CredentialsExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
}
to
Code:
if (!user.isCredentialsNonExpired()) {
logger.warn(user.getUsername() + " credentials expired");
}
I also created a new filter called ChangePasswordFilter, extending OncePerRequestFilter (it probably could just implement Filter)
Code:
public class ChangePasswordFilter extends OncePerRequestFilter implements Filter, InitializingBean {
protected final String ERRORS_KEY = "errors";
protected String changePasswordKey = "user.must.change.password";
private Log logger = LogFactory.getLog(getClass());
private String changePasswordUrl = null;
/*
* (non-Javadoc)
*
* @see javax.servlet.Filter#destroy()
*/
public void destroy() {
// TODO Auto-generated method stub
}
/*
* (non-Javadoc)
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws ServletException {
Assert.notNull(changePasswordUrl, "changePasswordUrl must be set.");
Assert.notNull(changePasswordKey, "changePasswordKey must be set.");
}
/*
* (non-Javadoc)
*
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
UserDetails userDetails = null;
String requestURL = request.getRequestURL().toString();
if (requestURL.endsWith(".html") || requestURL.endsWith(".do") || requestURL.endsWith(".jsp")) {
logger.debug("changepasswordfilter URL: " + request.getRequestURL());
logger.debug("changepasswordfilter URI: " + request.getRequestURI());
try {
Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (obj instanceof UserDetails) {
userDetails = (UserDetails) obj;
} else {
}
if (userDetails != null && userDetails.isCredentialsNonExpired() == false) {
// send user to change password page
logger.debug("credentials expired - sending to changepassword page.");
int pos = requestURL.indexOf("changepassword");
if (pos == -1) {
saveError(request, changePasswordKey);
sendRedirect(request, response, changePasswordUrl);
return;
}
}
} catch (Exception e) {
}
}
chain.doFilter(request, response);
}
/**
* The URL to the Change Password page. It must begin with a slash and should be relative
* from the application's contextPath root (ex: /changepassword.do).
* @param changePasswordUrl the changePasswordUrl to set
*/
public void setChangePasswordUrl(String changePasswordUrl) {
this.changePasswordUrl = changePasswordUrl;
}
/**
* Allow subclasses to modify the redirection message.
*
* @param request the request
* @param response the response
* @param url the URL to redirect to
*
* @throws IOException in the event of any failure
*/
protected void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url)
throws IOException {
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = request.getContextPath() + url;
}
response.sendRedirect(response.encodeRedirectURL(url));
}
public void saveError(HttpServletRequest request, String msg) {
Set errors = (Set) request.getSession().getAttribute(ERRORS_KEY);
if (errors == null) {
errors = new HashSet();
}
errors.add(msg);
request.getSession().setAttribute(ERRORS_KEY, errors);
}
/**
* The message bundle key that will hold the "You must change your password" error message.
* The default key name is <b>user.must.change.password</b>.
* @param changePasswordKey the changePasswordKey to set
*/
public void setChangePasswordKey(String changePasswordKey) {
this.changePasswordKey = changePasswordKey;
}
}
Then in my applicationContext-acegi-security.xml file, I added changePasswordFilter to the end of the filterInvocationDefinitionSource property of the filterChainProxy bean:
Code:
/images/**=#NONE#
/javascript/**=#NONE#
/css/**=#NONE#
/changepassword.do=channelProcessingFilter,httpSessionContextIntegrationFilterWithASCTrue,logoutFilter,authenticationProcessingFilter,securityContextHolderAwareRequestFilter,rememberMeProcessingFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
/**=channelProcessingFilter,httpSessionContextIntegrationFilterWithASCTrue,logoutFilter,authenticationProcessingFilter,securityContextHolderAwareRequestFilter,rememberMeProcessingFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor,changePasswordFilter
Code:
<bean id="changePasswordFilter"
class="com.abc.web.filter.ChangePasswordFilter">
<property name="changePasswordUrl" value="/changepassword.do" />
</bean>