I'm doing an application with authentication by OpenID using Spring Security. When user is logged-in, some authorities are loaded in his session.
I have User with full right which can modify authorities (revoke, add roles) of others users. My question is, how to change User session authorities dynamically ? (cannot use SecurityContextHolder because I want to change another User session).
Simple way : invalidate user session, but how to ? Better way : refresh user session with new authorities, but how to ?
If you need to dynamically update a logged in user's authorities (when these have changed, for whatever reason), without having to log out and log in of course, you just need to reset the Authentication
object (security token) in the Spring SecurityContextHolder
.
Example:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
List<GrantedAuthority> updatedAuthorities = new ArrayList<>(auth.getAuthorities());
updatedAuthorities.add(...); //add your role here [e.g., new SimpleGrantedAuthority("ROLE_NEW_ROLE")]
Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), updatedAuthorities);
SecurityContextHolder.getContext().setAuthentication(newAuth);
Thanks, help me a lot ! With SessionRegistry
, I can use getAllPrincipals() to compare the user to modify with the current active users in sessions. If a session exist, I can invalidate his session using : expireNow() (from SessionInformation
) to force re-authentication.
But I don't understand the usefulness of securityContextPersistenceFilter
?
EDIT :
// user object = User currently updated
// invalidate user session
List<Object> loggedUsers = sessionRegistry.getAllPrincipals();
for (Object principal : loggedUsers) {
if(principal instanceof User) {
final User loggedUser = (User) principal;
if(user.getUsername().equals(loggedUser.getUsername())) {
List<SessionInformation> sessionsInfo = sessionRegistry.getAllSessions(principal, false);
if(null != sessionsInfo && sessionsInfo.size() > 0) {
for (SessionInformation sessionInformation : sessionsInfo) {
LOGGER.info("Exprire now :" + sessionInformation.getSessionId());
sessionInformation.expireNow();
sessionRegistry.removeSessionInformation(sessionInformation.getSessionId());
// User is not forced to re-logging
}
}
}
}
}
securityContextPersistenceFilter
by default will put SecurityContext
into HttpSession
in servlet environment. As you already have out-of-the-box spring SessionRegistry
you do not need to customize this filter.
HttpSession
s are disabled and you don't want thread-local storage. So you can use your own implementation of securityContextRepository
. If HttpSession
storage suit your needs, then there is no usefulness.
SecurityContext
for each user lies in each user's session, see details here. If you can access other user's session through registry then you can do what you want with it.
If anybody is still looking into how to update the authorities of another user without forcing that user to re-authenticate, you can try to add an interceptor that reloads the authentication. This will make sure that your authorities are always updated.
However -- due to the extra interceptor, there will be some performance impacts (e.g. if you get your user roles from your database, it will be queried for every HTTP request).
@Component
public class VerifyAccessInterceptor implements HandlerInterceptor {
// ...
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Set<GrantedAuthority> authorities = new HashSet<>();
if (auth.isAuthenticated()) {
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
}
User userFromDatabase = getUserFromDatabase(auth.getName());
if (userFromDatabase != null) {
// add whatever authorities you want here
authorities.add(new SimpleGrantedAuthority("..."));
}
Authentication newAuth = null;
if (auth.getClass() == OAuth2AuthenticationToken.class) {
OAuth2User principal = ((OAuth2AuthenticationToken)auth).getPrincipal();
if (principal != null) {
newAuth = new OAuth2AuthenticationToken(principal, authorities,(((OAuth2AuthenticationToken)auth).getAuthorizedClientRegistrationId()));
}
}
SecurityContextHolder.getContext().setAuthentication(newAuth);
return true;
}
}
This specific implementation uses OAuth2 (OAuth2AuthenticationToken
), but you can use UsernamePasswordAuthenticationToken
instead.
And now, to add your interceptor to the configuration:
@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {
@Autowired
private VerifyAccessInterceptor verifyAccessInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(verifyAccessInterceptor).addPathPatterns("/**");
}
}
I also made an article about this.
The key point - you should be able to access users SecurityContext
s.
If you are in servlet environment and are using HttpSession
as securityContextRepository
in your securityContextPersistenceFilter
, then it can be done with spring's SessionRegistry
. To force the user to re-auth (it should be better than silent permissions revocation) invalidate his HttpSession
. Don't forget to add HttpSessionEventPublisher
to web.xml
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
If you are using thread-local securityContextRepository
, then you should add custom filter to springSecurityFilterChain
to manage SecurityContext
s registry. To do this you must the use plain-bean springSecurityFilterChain
configuration (without security
namespace shortcuts). With plain-bean config with custom filters you'll have full control on authentication and authorization.
Some links, they don't solve exactly your problem (no OpenID), but may be useful:
NIH session registry for servlet environment
it's plain-bean spring config working example
real life plain-bean spring config for X.509 auth, you may start with it and modify it to use OpenID instead of X.509.
I use the answer gived by TwiN, but I create a control variable (users_to_update_roles) to reduce performance impacts.
@Component
public class RoleCheckInterceptor implements HandlerInterceptor {
public static ArrayList<String> update_role = new ArrayList<>();
@Autowired
private IUser iuser;
public static Set<String> users_to_update_roles = new HashSet<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
try {
CurrentUser current = (CurrentUser) auth.getPrincipal();
String username = current.getUser().getUsername();
if (users_to_update_roles.contains(username)) {
updateRoles(auth, current);
users_to_update_roles.remove(username);
}
} catch (Exception e) {
// TODO: handle exception
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
private void updateRoles(Authentication auth, CurrentUser current) {
User findOne = iuser.findOne(current.getUser().getUsername());
List<GrantedAuthority> updatedAuthorities = new ArrayList<>();
for (Role role : findOne.getRoles()) {
updatedAuthorities.add(new SimpleGrantedAuthority(role.name()));
}
Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(),
updatedAuthorities);
SecurityContextHolder.getContext().setAuthentication(newAuth);
}
}
and in my controller, I add the user that have they role updated
public ModelAndView roleSave(@PathVariable long numero_documento, Funcionario funcionario) {
ModelAndView modelAndView = new ModelAndView("funcionario/role");
Set<Role> roles = funcionario.getPessoa().getUser().getRoles();
funcionario = funcionarioService.funcionarioNumero_documento(numero_documento);
funcionario.getPessoa().getUser().setRoles(roles);
iUser.save(funcionario.getPessoa().getUser());
RoleCheckInterceptor.users_to_update_roles.add(funcionario.getPessoa().getUser().getUsername());
modelAndView.addObject("funcionario", funcionario);
modelAndView.addObject("sucess", "Permissões modificadas");
return modelAndView;
}
ConcurrentHashMap
that maps username (or better user ID) to Long and put the value of AtomicLong.incrementAndGet()
in it when you change, and instead of remove by ID in preHandle
you use removeIf(value -> value.equals(valueRetrievedAtTheStartOfPreHandle))
?
I have a very specific case of above, I use Redis to track user session with https://github.com/spring-projects/spring-session. Then when admin adds some Role to the user I find user session in Redis and replace principal
and authorities
and then save the session.
public void updateUserRoles(String username, Set<GrantedAuthority> newRoles) {
if (sessionRepository instanceof FindByIndexNameSessionRepository) {
Map<String, org.springframework.session.Session> map =
((FindByIndexNameSessionRepository<org.springframework.session.Session>) sessionRepository)
.findByPrincipalName(username);
for (org.springframework.session.Session session : map.values()) {
if (!session.isExpired()) {
SecurityContext securityContext = session.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
Authentication authentication = securityContext.getAuthentication();
if (authentication instanceof UsernamePasswordAuthenticationToken) {
Collection<GrantedAuthority> authorities = new HashSet<>(authentication.getAuthorities());
//1. Update of authorities
authorities.addAll(newRoles);
Object principalToUpdate = authentication.getPrincipal();
if (principalToUpdate instanceof User) {
//2. Update of principal: Your User probably extends UserDetails so call here method that update roles to allow
// org.springframework.security.core.userdetails.UserDetails.getAuthorities return updated
// Set of GrantedAuthority
securityContext
.setAuthentication(new UsernamePasswordAuthenticationToken(principalToUpdate, authentication
.getCredentials(), authorities));
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, securityContext);
sessionRepository.save(session);
}
}
}
}
}
}
Success story sharing