57
votes

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 ?

6

6 Answers

55
votes

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

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

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.

8
votes

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:

1
votes

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;
}
1
votes

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