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

17,086

Solution 1

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));
        }
    }
}

Solution 2

I am not sure such functionality is provided by spring as in-built.

I have achieved similar things using setting one column in table which help me identify first time login by user or not.

If it is first time login then view to display in my case was reset Password page otherwise my dashboard page.

Solution 3

in ForceChangePasswordFilter, for stopping filter from loop you should check ServletPath containing ChangePassword url or not. like this:

    if(multiReadRequest.getServletPath().startsWith("/ChangePass.htm"))
       flag=false;
Share:
17,086
Jonathan
Author by

Jonathan

Jonathan as handles team leadership roles. He started to work from 2003, but his interest in programming started back to 1998 the firs time he solved a programming problem. His main programming language is Java and prefers developing web applications. He has working knowledge in Python, VBA, and Ruby, mainly to create productivity tools.

Updated on June 19, 2022

Comments

  • Jonathan
    Jonathan almost 2 years

    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?

  • Andrei Epure is hiring
    Andrei Epure is hiring almost 7 years
    isn't this a security flaw? Somebody else (an attacker) might try to login with fake credentials, and if the user should change his password, then the attacker gets to change the password of the not-yet-logged in user. Sure, you have to give the current password when changing it. But that means that the change-password endpoint is exposed to dictionary attacks, unless properly secured. This means the attack surface is greater - the normal login form and the change password form.