Skip to main content
Blog

Using SwitchUser functionality in Spring

By 25 augustus 2015januari 30th, 2017No Comments

If you are developing a website with external users, those users might require support. For the people providing support, it can be quite convenient to be able to log in as a user and see the application through their eyes. In this blog post we’ll visit some of the components Spring 3 provides to handle this kind of feature.

Switching user

In our application, a user can log in and edit properties for a company he is authorised for. The application uses Spring Security for that. The switch-user functionality (or su in unix terminology) is provided by Spring as a filter (SwitchUserFilter) which can be configured in your application.xml as follows:

 
  
  
  
  
  

We specify our own UserDetailsService since we have a list of users maintained a non-standard source. The bean definition also specifies two url’s, the switch and exit url, as well as a parameter. With these, we have now defined that a simple GET to /backoffice/sudo?user=someuser logs you in as someuser. Of course, we don’t want everyone to be able to do this. Only “ADMIN” users should be allowed to use this feature, so we have to protect that URL using standard Spring Security rules:

  
  

Note that the reverse switchUser (sundo) is protected by the role of the target user, otherwise we would get a permission denied when trying to go back.

Auditing

Spring implements this feature by changing the Authentication in the SecurityContext. The administrators authentication token in the SecurityToken is replaced with a token representing the target user. The authentication token has some authorities (also known as grants, roles, permissions, authorizations or whatever name your favorite framework uses). Of course, one authority is the ROLE_USER since, well, you are now a plain user. But, that is not all. Since you are actually pretending to be that user, you have an additional authority: ROLE_PREVIOUS_ADMINISTRATOR. This authority is of a different type (normally it would be something like SimpleGrantedAuthority,  nothing more than a wrapper around a String). This authority is of type SwitchUserGrantedAuthority and allows us to discover the original user.

We can use this for audit logging purposes for example. In our web.xml we’ve defined a filter which adds context for log lines using MDC (mapped diagnostic context). Here we can add the logged in user (and the actual user behind it if somebody sudo’ed). So we define a filter:

  userNameFilter
  nl.first8.web.UserNameFilter
  true


  userNameFilter
  /*

And in that filter we add some context for the user:

public class UserNameFilter implements Filter {
  @Override public void init(FilterConfig filterConfig) 
    throws ServletException {}
  @Override public void destroy() {}

  @Override
  public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) 
    throws IOException, ServletException {
    MDC.put("user", findUsername());
    MDC.put("ip", findIP(request));
    try {
        chain.doFilter(request, response);
    } finally {
        MDC.remove("user");
        MDC.remove("ip");
    }
  }

  private String findIP(final ServletRequest request) {
    return request.getRemoteAddr();
  }

  private String findUsername() {
    final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if( authentication == null ) {
      return "anonymous";
    } else {
      final String userName = authentication.getName();
      final String realName = findSwitchedUser(authentication);
      return userName + (realName != null ? " (" + realName + ")" : "");
    }
  }

  private String findSwitchedUser(Authentication authentication) {
    for (GrantedAuthority auth : authentication.getAuthorities()) {
      if (auth instanceof SwitchUserGrantedAuthority) {
        SwitchUserGrantedAuthority suAuth = (SwitchUserGrantedAuthority)auth;
        return suAuth.getSource().getName();
      }
    }
    return null;
  }
}

You can now use these context properties in your logging framework, e.g. you can define a log4j console appender like this:

log4j.appender.CONSOLE.layout.ConversionPattern=%d [%t][%X{ip}:%X{user}] %5p %c - %m%n

which could log something like this when an administrator is pretending to be a user:

2015-08-13 11:23:20,944 [http-bio-8080-exec-15][0:0:0:0:0:0:0:1:testuser (arjanl)] DEBUG nl.first8.web.DashboardController - Rendering account/dashboard.vm

Session cleanup

 A final thing to be aware of is that if an administrator switches to a user, he (or she) still keeps the same HttpSession. If the administrator switches regularly between users, some information might leak between sessions. In our example, users can switch between companies for whom they are authorized to do some work. By default no company is selected (currentCompanyId == null) and if they have selected a company, it is validated once, it is stored in a session scoped bean. From then on, it is assumed that that user has access to that company. So this is something that we don’t want to leak when an administrator switches users. The bean might look like this:

@Component
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS, value = "session")
public class UserAccountSession implements Serializable {
  private static final long serialVersionUID = 1L;

  private Long currentCompanyId;

  public Long getCurrentCompanyId() {
    return currentCompanyId;
  }

  public void setCurrentCompanyId(Long currentCompanyId) {
    this.currentCompanyId = currentCompanyId;
  }

  public void reset() {
    this.currentProviderId = null;
  }
}

So we need to reset this bean whenever a user switch happens. For this, we can register an ApplicationListener that listens to switch user events:

@Service
public class SwitchUserListener
  implements ApplicationListener {

  @Autowired
  private UserAccountSession userAccountSession;

  @Override
  public void onApplicationEvent(AuthenticationSwitchUserEvent event) {
    userAccountSession.reset();
  }
}

Note: you might run into trouble here: the userAccountSession is, well, session scoped. So you might need run into an error like below:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.accountSession': Scope 'session' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.

This can be fixed by adding the RequestContextListener to web.xml which exposes the web layer’s request context to other layers:

  
  org.springframework.web.context.request.RequestContextListener

With this, you should have a properly configured sudo functionality.

Read more: