How to Enforce Change Password on User's initial login using Spring Security

Jonathan picture Jonathan · Jul 25, 2013 · Viewed 16.1k times · Source

What would be the most elegant way of implementing a force password change upon user's initial login using Spring Security?

I tried implementing a custom AuthenticationSuccessHandler as mentioned here, but as mentioned by rodrigoap, if a user manually inputs the URL at the address bar, the user will still be able to proceed to that page even if he didn't change his password.

I did this with a filter ForceChangePasswordFilter. Because if the user types the url by hand they can bypass the change password form. With the filter the request always get intercepted.

As such, I proceeded with implementing a custom filter.

My question is this, when I implement a custom filter and send a redirect inside it, it goes through the filter again causing an infinite redirect loop as mentioned here. I tried implementing the solution mentioned by declaring two http tags in my security-context.xml with the first tag having the pattern attribute as such but it still goes through my custom filter:

<http pattern="/resources" security="none"/>
<http use-expressions="true" once-per-request="false"
    auto-config="true">
  <intercept-url pattern="/soapServices/**" access="permitAll" requires-channel="https"/>
  ...
  <custom-filter position="LAST" ref="passwordChangeFilter" />
</http>
...
<beans:bean id="passwordChangeFilter"
  class="my.package.ForcePasswordChangeFilter"/>
<beans:bean id="customAuthenticationSuccessHandler"
  class="my.package.CustomAuthenticationSuccessHandler" >
</beans:bean>
<beans:bean id="customAuthenticationFailureHandler"
  class="my.package.CustomAuthenticationFailureHandler" >
  <beans:property name="defaultFailureUrl" value="/login"/>
</beans:bean>

What my current implementation is (which works) is:

  • Inside my custom authentication success handler, I set a session attribute isFirstLogin
  • In my ForcePasswordChangeFilter, I check if the session isFirstLogin is set
    • If it is, then I send a redirect to my force password change
    • Else, I call chain.doFilter()

My problem with this implementation is that access to my resources folder also goes through this filter which causes my page to be distorted (because *.js and *.css are not successfully retrieved). This is the reason I tried having two <http> tags in my security app context.xml (which didn't work).

As such, I ended up having to manually filter the request if the servletPath starts or contains "/resources". I didn't want it to be like this - having to manually filter the request path - but for now it's what I have.

What's the more elegant way of doing this?

Answer

sezerug picture sezerug · Jul 29, 2013

I solved this issue by providing a status value for the user,

  • status=-1 ; initial login
  • status=0 ; deactive account
  • status=1 ; active account

and 2 custom authentication controller in the security.xml. First for to check username, pass and second for the additional controls like initial login, password expiration policy.

In case of first login, providing correct values of username and password, first controller (user-service-ref="jdbcUserService") fails to authenticate user(because user's status=-1) than second controller(ref="myAuthenticationController") catches the request. In this controller DisabledException is thrown.

Finally, you can redirect user to password-change page on AuthenticationFailureListener's onAuthenticationFailure method.

A part of security.xml

<authentication-manager alias="authenticationManager">
    <authentication-provider user-service-ref="jdbcUserService">
        <password-encoder ref="passwordEncoder" />
    </authentication-provider>
    <authentication-provider ref="myAuthenticationController" />
</authentication-manager>

<beans:bean id="jdbcUserService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
    <beans:property name="rolePrefix" value="ROLE_" />
    <beans:property name="dataSource" ref="dataSource" />
    <beans:property name="usersByUsernameQuery" value="SELECT user_name as userName, PASSWORD as password, STATUS as status FROM  USER WHERE  user_name = ? AND STATUS=1" />
    <beans:property name="authoritiesByUsernameQuery" value="SELECT user_name as userName, ROLE as authority FROM USER WHERE user_name = ?" />
</beans:bean>

<beans:bean id="myAuthenticationController" class="com.test.myAuthenticationController">
    <beans:property name="adminUser" value="admin" />
    <beans:property name="adminPassword" value="admin" />
</beans:bean>

<!--Custom authentication success handler for logging/locking/redirecting-->

<beans:bean id="authSuccessHandler" class="com.test.AuthenticationSuccessListener"/>

<!--Custom authentication failure handler for logging/locking/redirecting-->

<beans:bean id="authFailureHandler" class="com.test.AuthenticationFailureListener"/>

@Service("myAuthenticationController")
public class MyAuthenticationController extends AbstractUserDetailsAuthenticationProvider {

    private final Logger logger = Logger.getLogger(getClass());

    @Autowired
    private WfmUserValidator userValidator;
    private String username;
    private String password;

    @Required
    public void setAdminUser(String username) {
        this.username = username;
    }

    @Required
    public void setAdminPassword(String password) {
        this.password = password;
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        return;
    }

    @Override
    protected UserDetails retrieveUser(String userName, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        String password = (String) authentication.getCredentials();
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        String userRole = "";


        if (status = -1) {
            throw new DisabledException("It is first login. Password change is required!");
        } else if (password expired) {
            throw new CredentialsExpiredException("Password is expired. Please change it!");
        }

        return new User(userName, password, true, // enabled
                true, // account not expired
                true, // credentials not expired
                true, // account not locked
                authorities);
    }
}

public class AuthenticationFailureListener implements AuthenticationFailureHandler {

    private static Logger logger = Logger.getLogger(AuthenticationFailureListener.class);
    private static final String BAD_CREDENTIALS_MESSAGE = "bad_credentials_message";
    private static final String CREDENTIALS_EXPIRED_MESSAGE = "credentials_expired_message";
    private static final String DISABLED_MESSAGE = "disabled_message";
    private static final String LOCKED_MESSAGE = "locked_message";

    @Override
    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, AuthenticationException ex) throws IOException, ServletException {
        // TODO Auto-generated method stub
        String userName = req.getParameter("j_username");
        logger.info("[AuthenticationFailure]:" + " [Username]:" + userName + " [Error message]:" + ex.getMessage());

        if (ex instanceof BadCredentialsException) {
            res.sendRedirect("../pages/login.jsf?message=" + MessageFactory.getMessageValue(BAD_CREDENTIALS_MESSAGE));
        } else if (ex instanceof CredentialsExpiredException) {
            res.sendRedirect("../pages/changecredentials.jsf?message=" + MessageFactory.getMessageValue(CREDENTIALS_EXPIRED_MESSAGE));
        } else if (ex instanceof DisabledException) {
            res.sendRedirect("../pages/changecredentials.jsf?message=" + MessageFactory.getMessageValue(DISABLED_MESSAGE));
        } else if (ex instanceof LockedException) {
            res.sendRedirect("../pages/login.jsf?message=" + MessageFactory.getMessageValue(LOCKED_MESSAGE));
        }
    }
}