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:
isFirstLogin
isFirstLogin
is set
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?
I solved this issue by providing a status value for the user,
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));
}
}
}