How to reload authorities on user update with Spring Security

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 ?


Solution 1:

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

Solution 2:

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

Solution 3:

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.

Solution 4:

The key point - you should be able to access users SecurityContexts.

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 SecurityContexts 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.