Announcement Announcement Module
Collapse
No announcement yet.
Forcing user to change expired password Page Title Module
Move Remove Collapse
This topic is closed
X
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • 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...piredException

    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
    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>

    Comment


    • #3
      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)

      Comment


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

        Comment


        • #5
          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.

          Comment


          • #6
            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...

            Comment


            • #7
              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!

              Comment


              • #8
                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?

                Comment


                • #9
                  User is my own. It implements Acegi's UserDetails

                  Code:
                  public class User implements UserDetails

                  Comment


                  • #10
                    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

                    Comment


                    • #11
                      Problem solve with a filter

                      I had a similar requirement to, but i have to force the user to change the password or the email. I ended up creating a filter that when an AccessDeniedException is raised, check if a ROLE_XXXX is in the authorities for a user, if it is it redirect them to a roleXXXCustomAccesDeniedpage. I put the filter after the EXCEPTION_TRANSLATION_FILTER, so it catch the exception before the EXCEPTION_TRANSLATION_FILTER,if the filter find any of the custom role in the authorities list the redirect the user to its page and eat up the exception, if not rethrow it and let the EXCEPTION_TRANSLATION_FILTER handle it.

                      Code:
                       
                      <beans:bean id="CustomExceptionTranslationFilter"     
                                     class="security.CustomExceptionTranslationFilter">
                         <custom-filter after="EXCEPTION_TRANSLATION_FILTER"/>
                             <beans:property name="roleHandling">
                      	 <beans:map>
                           	    <beans:entry key="ROLE_CHANGEMAIL" value="/forceMail.htm"/>
                      	    <beans:entry key="ROLE_CHANGEPASS" value="/forcePass.htm"/>
                               </beans:map>
                             </beans:property>
                      </beans:bean>

                      Code:
                      public class CustomExceptionTranslationFilter extends SpringSecurityFilter implements InitializingBean {
                      	private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
                      	private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
                      	private boolean createSessionAllowed = true;
                      	private final Map<String, String> roleHandling = new HashMap<String, String>();
                      
                      	public void afterPropertiesSet() throws Exception {
                      		Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver must be specified");
                      		Assert.notNull(throwableAnalyzer, "throwableAnalyzer must be specified");
                      	}
                      
                      	@Override
                      	public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException,
                      	ServletException {
                      		try {
                      			chain.doFilter(request, response);
                      		}
                      		catch (IOException ex) {
                      			throw ex;
                      		}
                      		catch (Exception ex) {
                      			// Try to extract a SpringSecurityException from the stacktrace
                      			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
                      			SpringSecurityException ase = (SpringSecurityException) throwableAnalyzer.getFirstThrowableOfType(SpringSecurityException.class, causeChain);
                      
                      			if (ase != null) {
                      				if(handleException(request, response, chain, ase)){//check if the exception is in the roleHandling map.
                      					return;
                      				}else {
                      					// Rethrow ServletExceptions and RuntimeExceptions as-is
                      					if (ex instanceof ServletException) {
                      						throw (ServletException) ex;
                      					}
                      					else if (ex instanceof RuntimeException) {
                      						throw (RuntimeException) ex;
                      					}
                      
                      					// Wrap other Exceptions. These are not expected to happen
                      					throw new RuntimeException(ex);
                      				}
                      			}else {
                      			}
                      		}
                      	}
                      
                      
                      	private boolean handleException(ServletRequest request, ServletResponse response, FilterChain chain,
                      			SpringSecurityException exception) throws IOException, ServletException {
                      
                      		if(exception instanceof AccessDeniedException) {
                      			if (!authenticationTrustResolver.isAnonymous(SecurityContextHolder.getContext().getAuthentication())) {
                      				for (GrantedAuthority authority :SecurityContextHolder.getContext().getAuthentication().getAuthorities()){
                      					if (roleHandling.containsKey(authority.getAuthority())){
                      						String url = roleHandling.get(authority.getAuthority());
                      						RequestDispatcher rd = request.getRequestDispatcher(url);
                      						rd.forward(request, response);
                      						return true;
                      					}
                      				}
                      			}
                      		}
                      		return false;
                      	}
                      
                      
                      
                      	public void setRoleHandling(Map<String, String> mappings) {
                      		roleHandling.putAll(mappings);
                      	}
                      
                      	/**
                      	 * If <code>true</code>, indicates that <code>SecurityEnforcementFilter</code> is permitted to store the target
                      	 * URL and exception information in the <code>HttpSession</code> (the default).
                      	 * In situations where you do not wish to unnecessarily create <code>HttpSession</code>s - because the user agent
                      	 * will know the failed URL, such as with BASIC or Digest authentication - you may wish to
                      	 * set this property to <code>false</code>. Remember to also set the
                      	 * {@link org.springframework.security.context.HttpSessionContextIntegrationFilter#allowSessionCreation}
                      	 * to <code>false</code> if you set this property to <code>false</code>.
                      	 *
                      	 * @return <code>true</code> if the <code>HttpSession</code> will be
                      	 * used to store information about the failed request, <code>false</code>
                      	 * if the <code>HttpSession</code> will not be used
                      	 */
                      	public boolean isCreateSessionAllowed() {
                      		return createSessionAllowed;
                      	}
                      
                      
                      	public AuthenticationTrustResolver getAuthenticationTrustResolver() {
                      		return authenticationTrustResolver;
                      	}
                      
                      	public void setAuthenticationTrustResolver(AuthenticationTrustResolver authenticationTrustResolver) {
                      		this.authenticationTrustResolver = authenticationTrustResolver;
                      	}
                      
                      	public void setCreateSessionAllowed(boolean createSessionAllowed) {
                      		this.createSessionAllowed = createSessionAllowed;
                      	}
                      
                      	public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
                      		this.throwableAnalyzer = throwableAnalyzer;
                      	}
                      
                      	public int getOrder() {
                      		return FilterChainOrder.EXCEPTION_TRANSLATION_FILTER;
                      	}
                      
                      	/**
                      	 * Default implementation of <code>ThrowableAnalyzer</code> which is capable of also unwrapping
                      	 * <code>ServletException</code>s.
                      	 */
                      	private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
                      		/**
                      		 * @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap()
                      		 */
                      		@Override
                      		protected void initExtractorMap() {
                      			super.initExtractorMap();
                      
                      			registerExtractor(ServletException.class, new ThrowableCauseExtractor() {
                      				public Throwable extractCause(Throwable throwable) {
                      					ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
                      					return ((ServletException) throwable).getRootCause();
                      				}
                      			});
                      		}
                      
                      	}
                      
                      }

                      Comment


                      • #12
                        What about this solution with exceptionMappings

                        take a look at
                        acegisecurity.org/acegi-security/apidocs/org/acegisecurity/ui/AbstractProcessingFilter.html[/url]
                        for more info.

                        <bean id="casProcessingFilter"
                        class="org.springframework.security.ui.cas.CasProc essingFilter">
                        <sec:custom-filter after="CAS_PROCESSING_FILTER" />
                        <property name="authenticationManager"
                        ref="authenticationManager" />
                        <property name="exceptionMappings">
                        <props>
                        <prop key="org.springframework.security.CredentialsExpir edException">/credentialsExpire.jsp</prop>
                        <prop key="org.springframework.security.AccountExpiredEx ception">/accountExpired.jsp</prop>
                        <prop key="org.springframework.security.LockedException" >/accountLocked.jsp</prop>
                        </props>
                        </property>
                        <property name="authenticationFailureUrl" value="/casfailed.jsp" />
                        <property name="defaultTargetUrl" value="/" />
                        <property name="proxyGrantingTicketStorage"
                        ref="proxyGrantingTicketStorage" />
                        <property name="proxyReceptorUrl" value="/secure/receptor" />
                        </bean>

                        Comment


                        • #13
                          This is my solution.

                          Thanx splashout. Your post helps me soooooooo much.

                          first of all. I don't want to create a new class or override methods or something like that.

                          I want to solve this problem with addtional 5~10 lines of code.

                          so this is my solution.

                          import org.springframework.security.userdetails.User;
                          import org.springframework.security.providers.UsernamePas swordAuthenticationToken;

                          ...
                          change password & some codes & blah blah
                          ...


                          SecurityContext ctx = SecurityContextHolder.getContext();
                          Authentication auth = ctx.getAuthentication();

                          //get user info from db.(password changed)
                          MyUser myUser = myDao.getUser( "myID" );

                          //get user info from SecurityContext
                          User secUser = (User)auth.getPrincipal();

                          //create new instance for update.
                          secUser = new User( myUser.getId(), myUser.getPasswd(), true, true, true, true, secUser.getAuthorities() );

                          UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken( secUser, auth.getCredentials(), secUser.getAuthorities() );
                          upat.setDetails( auth.getDetails() );
                          ctx.setAuthentication( upat );
                          I do not know this is best. but it works.

                          if you guys find something better, let me know~~
                          Last edited by svaha; Jun 25th, 2009, 05:42 AM.

                          Comment


                          • #14
                            hi splashout,

                            i have created password change filter in my application after gone threw you posts. but stuck with how to configure filterchain proxy in my security.xml as i currentry i dont have one. please help.

                            Comment


                            • #15
                              Yeah, they changed the method of configuration. Much of it is documented and much is not (usually you can find the "not" part somewhere around here).

                              @see http://static.springsource.org/sprin...ngsecurity.pdf

                              I believe the you would need something like this in your config:

                              Code:
                              <beans:bean id="changePasswordFilter"  class="com.abc.web.filter.ChangePasswordFilter">
                              	<custom-filter position="LAST"/>
                              </beans:bean>

                              Comment

                              Working...
                              X