Page 1 of 2 12 LastLast
Results 1 to 10 of 15

Thread: Forcing user to change expired password

  1. #1

    Default Forcing user to change expired password

    I know there have been several threads about how to force a user to change an expired password, but all of them require you to override a lot of base Acegi classes because the AbstractUserDetailsAuthenticationProvider.authenti cate() method throws CredentialsExpiredException before the user is authenticated.
    http://forum.springframework.org/sho...hange+password
    http://forum.springframework.org/sho...hange+password
    http://forum.springframework.org/sho...hange+password
    http://forum.springframework.org/sho...re dException

    It seems like the only way to let a user user sign in and then force them to change their password is to:

    1. Override AbstractDaoAuthenticationProvider.authenticate and comment out:
    Code:
    if (!user.isCredentialsNonExpired()) {
      throw new CredentialsExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
    }
    2. Create a new filter and add it to the end of the filter proxy chain and redirect the user to a changePassword page if userDetails.isCredentialsExpired() == false.

    Has anyone implemented this functionality without overriding the authenticate() method?

    Thanks

  2. #2

    Default

    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>

  3. #3

    Default

    Thanks for posting this mstralka. I will try your filter, but rather than override authenticate(), I'm going to try make use of my User class (subclass of UserDetails) that will have the following properties:

    isCredentialsNonExpired(): will always returns true (to avoid the Exception)
    isPasswordExpired(): will be the method used by the filter

    So...
    if (userDetails != null && userDetails.isCredentialsNonExpired() == false)

    becomes...
    if (user != null && user.isPasswordExpired() == true)

  4. #4

    Default

    Splashout - your solution is a better idea - thanks for posting. I'm going to change mine to do the same

  5. #5
    Join Date
    Jan 2007
    Posts
    23

    Default

    I ended up with a similar requirement recently. I had to create my own UserDetailsService implementation anyway and needed to provide several custom checks. For password changing my UserDetailsService makes the decision and if a password change is required, it adds a ROLE_PASSWORD_CHANGE to the UserDetail object's authorities.

    I then have a simple filter setup that redirects any user with ROLE_PASSWORD_CHANGE to a password change form. That way the user is successfully authenticated but they can't go anywhere but the password change form. The password change form removes ROLE_PASSWORD_CHANGE once the password is successfully changed.

  6. #6

    Default

    Thanks cmose. I thought about that strategy but, unless I misunderstand, the filter will always redirect a user who has a property of (passwordExpired == true) to the changepassword page which means they can't hit any other page even by typing a url.

    This is why I had to update the SecurityContextHolder once the user changed the password (see below) -- so, they can use the site after the password is changed. I've been testing this and it seems to work as expected.

    Code:
    // gonna need this to get user from Acegi
    SecurityContext ctx = SecurityContextHolder.getContext();
    Authentication auth = ctx.getAuthentication();
    		
    		
    // get user obj
    User user = (User) auth.getPrincipal();
    
    // update the password on the user obj
    user.setPassword(password);
    user.setPasswordExpired(false);
    
    // Tell Acegi about the changes:  update the SecurityContextHolder
    // (see  org.acegisecurity.providers.dao.AbstractUserDetailsAuthenticationProvider.createSuccessAuthentication()) 
    UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(user, auth.getCredentials(), user.getAuthorities());
    upat.setDetails(auth.getDetails());
    ctx.setAuthentication(upat);
    
    // don't forget to update the database...

  7. #7
    Join Date
    Jan 2007
    Posts
    23

    Default

    that's true - to address that, my controller responsible for actually handling the password change, removes the ROLE_PASSWORD_CHANGE from the user's granted authorities and then redirects the user to the originally requested page or to the "home" page if the original request was the login page.

    d'oh, didn't read fully through your reply - that's exactly what I had done. meant to convey that more clearly in my original post!

  8. #8
    Join Date
    Aug 2007
    Location
    Gliwice, Poland
    Posts
    14

    Default

    splashout - what type is your "user" object? If it's the default org.acegisecurity.userdetails.User, how come you can set it's password? If it's your own class extending it - shouldn't the object be immutable anyway?

    I double checked if the api specification that mentions this isn't out of date, so whatever does "update the ContextHolder" mean?

  9. #9

    Default

    User is my own. It implements Acegi's UserDetails

    Code:
    public class User implements UserDetails

  10. #10
    Join Date
    Aug 2007
    Location
    Gliwice, Poland
    Posts
    14

    Default

    Thanks for the reply.

    I honestly don't see the point of making the UserDetails implementation immutable, but I trust the Mighty Designers. I'm still using the built-in org.acegisecurity.userdetails.User and ended up creating new User and Authentication objects based on the old ones.

    Just for clarity - there's a similar topic:
    http://forum.springframework.org/showthread.php?t=51848

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •