I have been looking for a way to reload our Spring Security UserDetails object every request and cannot find an example anywhere.
Does anyone know how to do such a t
Hello so wanted to share something related to this issue with Token based authentication, in my case Oauth2. At first I tried the approach of hooknc above, in my case I was using Token based Authentication, so my Authentication object was instanceOf Oauth2Authentication. Unlike standard Authentication Principal, Oauth2Authentication object is constructed of the Authorization Request and Authentication object. Additionally the principal is constructed by using the token itself. So when trying to reuse the token in another call it would end up with old user data in principal. So this approach does not work for token based authentication.
My Original problem to be clear was after the user would update user settings, if the user would make other API calls after, it would result in the old user info. Instead of trying to update the principal I found issuing a new token after update was a better approach.
I also should add that my Authentication Oauth2 scheme is totally stateless, everything is stored in DB.
I'm tring FilterSecurityInterceptor's re-authenticate trick
for form-login with JDBC userDetailsService
AuthenticationProvider
authenticated to false.
package studying.spring;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
public class MyDaoAuthenticationProvider extends DaoAuthenticationProvider {
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
Authentication result = super.createSuccessAuthentication(principal, authentication, user);
result.setAuthenticated(false);
return result;
}
}
AuthenticationEntryPoint for ExceptionTranslationFilter
logout and redirect to login page.
package studying.spring;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
public class MyLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
public MyLoginUrlAuthenticationEntryPoint(String loginFormUrl) {
super(loginFormUrl);
}
@Override
protected String determineUrlToUseForThisRequest(
HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
if (exception != null) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
new SecurityContextLogoutHandler().logout(request, response, auth);
SecurityContextHolder.getContext().setAuthentication(null);
}
return super.determineUrlToUseForThisRequest(request, response, exception);
}
}
root-context.xml
rase-credentials attr. to false.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.1.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
<security:http entry-point-ref="myLoginUrlAuthenticationEntryPoint">
<security:intercept-url pattern="/**" access="hasRole('USER')"/>
<security:form-login/>
<security:logout/>
</security:http>
<security:authentication-manager erase-credentials="false">
<security:authentication-provider ref="myDaoAuthenticationProvider"/>
</security:authentication-manager>
<bean id="myLoginUrlAuthenticationEntryPoint" class="studying.spring.MyLoginUrlAuthenticationEntryPoint">
<constructor-arg name="loginFormUrl" value="/login" />
</bean>
<bean id="myDaoAuthenticationProvider" class="studying.spring.MyDaoAuthenticationProvider">
<property name="userDetailsService" ref="jdbcDaoImpl" />
</bean>
<bean id="jdbcDaoImpl" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="securityDataSource" />
</bean>
<bean id="securityDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/studying_spring_security" />
<property name="username" value="root" />
<property name="password" value="" />
</bean>
</beans>
For the side of the admin user changing the other user's authority: You can try to retrieve the sessions of the affected user and set some attribute to indicate a reload is needed.
If you happen to use Spring Session with session attributes persisted in the database (e.g., to support multiple container instances), you can tag an active session for reload when the admin user makes changes to the authorities:
@Autowired
private FindByIndexNameSessionRepository<Session> sessionRepo;
public void tag(String username) {
Map<String, Session> sessions = sessionRepo.findByIndexNameAndIndexValue
(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
for (Session s : sessions.values()) {
s.setAttribute("reloadAuth", true);
sessionRepo.save(s);
}
}
For the logged-in user side: You can write your Spring Security Filter that will check for the session attribute whether to reload the current session's authentication or not. If an admin has tagged it for reload, we retrieve again the Principal
from the DB and re-set our Authentication
.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpSession session = ((HttpServletRequest) request).getSession();
if (session != null) {
Boolean reload = (Boolean) session.getAttribute("reloadAuth");
if (Boolean.TRUE.equals(shouldReloadRoles)) {
session.removeAttribute("reloadAuth");
/* Do some locking based on session ID if you want just to avoid multiple reloads for a session */
Authentication newAuth = ... // Load new authentication from DB
SecurityContextHolder.getContext().setAuthentication(newAuth);
}
}
chain.doFilter(request, response);
}
Finally, after two years, for the question above and for six years after this question, here is an answer on how to reload a user's UserDetails per request with Spring...
To reload a user/security context per request, it is important to override the default behavior of Spring Security's HttpSessionSecurityContextRepository, which implements the SecurityContextRepository interface.
The HttpSessionSecurityContextRepository is the class that is used by Spring Security to get the user's security context from the HttpSession. The code that calls this class is what places the SecurityContext on threadlocal. So when the loadContext(HttpRequestResponseHolder requestResponseHolder)
method is called we can turn around and make a request to a DAO or Repository and reload the user/principal.
Some things of concern that have not quite been figured out quite yet.
Is this code thread safe?
I have no idea, it depends on if there is a new SecurityContext created per thread/request into the web server. If there is a new SecurityContext created life is good, but if not, there could be some interesting unexpected behavior like stale object exceptions, the wrong state for a user/principal being saved to the data store, etc...
Our code is 'low risk enough' that we haven't tried to test potential multi-thread issues.
Is there a performance hit for calling to the database every request?
Most likely, but we haven't seen noticeable change in our web server response times.
A couple of quick notes on this subject...
Benefits we've received from this change:
The Code
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.openid.OpenIDAuthenticationToken;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import xxx.repository.security.UserRepository;
import xxx.model.security.User;
import xxx.service.security.impl.acegi.AcegiUserDetails;
public class ReloadUserPerRequestHttpSessionSecurityContextRepository extends HttpSessionSecurityContextRepository {
// Your particular data store object would be used here...
private UserRepository userRepository;
public ReloadUserPerRequestHttpSessionSecurityContextRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
// Let the parent class actually get the SecurityContext from the HTTPSession first.
SecurityContext context = super.loadContext(requestResponseHolder);
Authentication authentication = context.getAuthentication();
// We have two types of logins for our system, username/password
// and Openid, you will have to specialize this code for your particular application.
if (authentication instanceof UsernamePasswordAuthenticationToken) {
UserDetails userDetails = this.createNewUserDetailsFromPrincipal(authentication.getPrincipal());
// Create a new Authentication object, Authentications are immutable.
UsernamePasswordAuthenticationToken newAuthentication = new UsernamePasswordAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities());
context.setAuthentication(newAuthentication);
} else if (authentication instanceof OpenIDAuthenticationToken) {
UserDetails userDetails = this.createNewUserDetailsFromPrincipal(authentication.getPrincipal());
OpenIDAuthenticationToken openidAuthenticationToken = (OpenIDAuthenticationToken) authentication;
// Create a new Authentication object, Authentications are immutable.
OpenIDAuthenticationToken newAuthentication = new OpenIDAuthenticationToken(userDetails, userDetails.getAuthorities(), openidAuthenticationToken.getIdentityUrl(), openidAuthenticationToken.getAttributes());
context.setAuthentication(newAuthentication);
}
return context;
}
private UserDetails createNewUserDetailsFromPrincipal(Object principal) {
// This is the class we use to implement the Spring Security UserDetails interface.
AcegiUserDetails userDetails = (AcegiUserDetails) principal;
User user = this.userRepository.getUserFromSecondaryCache(userDetails.getUserIdentifier());
// NOTE: We create a new UserDetails by passing in our non-serializable object 'User', but that object in the AcegiUserDetails is transient.
// We use a UUID (which is serializable) to reload the user. See the userDetails.getUserIdentifier() method above.
userDetails = new AcegiUserDetails(user);
return userDetails;
}
}
To plug a new SecurityContextRepository with xml configuration, just set the security-context-repository-ref attribute on the security:http context.
Example xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.0.xsd">
<security:http context-repository-ref="securityContextRepository" >
<!-- intercept-url and other security configuration here... -->
</security:http>
<bean id="securityContextRepository" class="xxx.security.impl.spring.ReloadUserPerRequestHttpSessionSecurityContextRepository" >
<constructor-arg index="0" ref="userRepository"/>
</bean>
</beans>