Spring REST endpoint to impersonate

Goal

Expose a REST endpoint to permit a user to impersonate other user in a Spring application

Description

In one of the projects I worked recently, we had a Spring based application exposing REST services that were consumed by an AngularJS client (that part, however, is not relevant for this specific recipe but it’s just to put the example into the appropriate context).

And in that project, there are some requirements regarding the impersonation of users. Notice that I decided not to use the SwitchUserFilter from Spring itself but to mimic its code in my own REST service, because of the security requirement stated below and also because I wanted to provide the client interface with a JSON answer regarding the impersonation answer):

  1. Auditing – operations that are performed on behalf of someone should be marked as such, in the format as
  2. Security – only authorized users are allowed to represent someone, i.e., in this project, if one wants to be impersonated by someone, s/he must create a delegation record on the database, giving permissions for a user to impersonate her/him during a specific period in time

This recipe details the basic steps of the implementation I made in order to achieve this result.

How to

  1. Create an audit filter to filter the requests and put in context the appropriate username (we are using Hibernate envers for auditing the changes in the persistent domain model. Notice, however, that I won’t go into the details of the Hibernate envers auditing within the project because that is not the focus of this recipe):
    import java.io.IOException;
    import java.util.Collection;
    
    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.http.HttpServletRequest;
    
    import org.springframework.security.authentication.AnonymousAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.context.SecurityContext;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
    
    import myprj.domain.impl.audit.listener.DomainContext;
    import myprj.domain.impl.audit.listener.DomainThreadLocal;
    
    /**
    *
    * @author Paulo Zenida - Linkare TI
    *
    */
    @WebFilter("/rest/*")
    public class AuditFilter implements Filter {
    
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    try {
    final String username = getUsername();
    DomainThreadLocal.setDomainContext(new DomainContext(username, request.getRemoteHost(), ((HttpServletRequest) request).getRequestURI()));
    chain.doFilter(request, response);
    } finally {
    if (DomainThreadLocal.getDomainContext() != null) {
    DomainThreadLocal.removeDomainContext();
    }
    }
    }
    
    private boolean isAuthenticated() {
    final SecurityContext context = SecurityContextHolder.getContext();
    if (context.getAuthentication() == null || context.getAuthentication() instanceof AnonymousAuthenticationToken) {
    return false;
    }
    return context.getAuthentication().isAuthenticated();
    }
    
    private String getUsername() {
    if (isAuthenticated()) {
    final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    
    final UserDetails originalUser = getOriginalUserIfAny(authentication);
    final UserDetails currentUser = (UserDetails) authentication.getPrincipal();
    
    if (originalUser != null) {
    return originalUser.getUsername() + " AS " + currentUser.getUsername();
    }
    return currentUser.getUsername();
    }
    return null;
    }
    
    private UserDetails getOriginalUserIfAny(final Authentication authentication) {
    Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
    
    for (GrantedAuthority grantedAuthority : authorities) {
    if (grantedAuthority instanceof SwitchUserGrantedAuthority) {
    final SwitchUserGrantedAuthority switchUser = (SwitchUserGrantedAuthority) grantedAuthority;
    return (UserDetails) switchUser.getSource().getPrincipal();
    }
    }
    return null;
    }
    
    @Override
    public void destroy() {
    }
    
  2. REST controller containing two operations, login() and logout() with security constraints defined (@PreAuthorize with hasPermission() expressions)
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    
    import javax.inject.Inject;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.context.ApplicationEventPublisher;
    import org.springframework.context.support.MessageSourceAccessor;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
    import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
    import org.springframework.security.authentication.AuthenticationDetailsSource;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.SpringSecurityMessageSource;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsChecker;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
    import org.springframework.security.web.authentication.switchuser.AuthenticationSwitchUserEvent;
    import org.springframework.security.web.authentication.switchuser.SwitchUserAuthorityChanger;
    import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
    import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority;
    import org.springframework.web.bind.annotation.CrossOrigin;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
    *
    * @author Paulo Zenida - Linkare TI
    *
    */
    @CrossOrigin(origins = "*")
    @RestController
    @RequestMapping("/impersonate")
    public class SwitchUserRestController extends BaseRestController {
    
    private final Log logger = LogFactory.getLog(getClass());
    
    @Inject
    private UserDetailsService userDetailsService;
    
    private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
    
    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    
    private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    
    private ApplicationEventPublisher eventPublisher;
    
    private SwitchUserAuthorityChanger switchUserAuthorityChanger;
    
    @PreAuthorize("hasPermission(#username, 'org.springframework.security.core.context.SecurityContext', 'ADMINISTRATION')")
    @RequestMapping(value = "login", method = RequestMethod.GET)
    public UserDetails login(@RequestParam(name = "username") final String username, final HttpServletRequest request) {
    final Authentication targetUser = attemptSwitchUser(username, request);
    
    // update the current context to the new target user
    SecurityContextHolder.getContext().setAuthentication(targetUser);
    return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
    
    @PreAuthorize("hasPermission('org.springframework.security.core.context.SecurityContext', 'ADMINISTRATION')")
    @RequestMapping(value = "logout", method = RequestMethod.GET)
    public UserDetails logout(final HttpServletRequest request, final HttpServletResponse response) {
    // get the original authentication object (if exists)
    final Authentication originalUser = attemptExitUser(request);
    
    // update the current context back to the original user
    SecurityContextHolder.getContext().setAuthentication(originalUser);
    return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
    
    private Authentication attemptSwitchUser(final String username, final HttpServletRequest request) throws AuthenticationException {
    UsernamePasswordAuthenticationToken targetUserRequest;
    
    UserDetails targetUser = userDetailsService.loadUserByUsername(username);
    userDetailsChecker.check(targetUser);
    
    // OK, create the switch user token
    targetUserRequest = createSwitchUserToken(request, targetUser);
    
    // publish event
    if (this.eventPublisher != null) {
    eventPublisher.publishEvent(new AuthenticationSwitchUserEvent(SecurityContextHolder.getContext().getAuthentication(), targetUser));
    }
    
    return targetUserRequest;
    }
    
    private Authentication attemptExitUser(final HttpServletRequest request) throws AuthenticationCredentialsNotFoundException {
    // need to check to see if the current user has a SwitchUserGrantedAuthority
    Authentication current = SecurityContextHolder.getContext().getAuthentication();
    
    if (null == current) {
    throw new AuthenticationCredentialsNotFoundException(messages.getMessage("SwitchUserFilter.noCurrentUser",
    "No current user associated with this request"));
    }
    
    // check to see if the current user did actual switch to another user
    // if so, get the original source user so we can switch back
    Authentication original = getSourceAuthentication(current);
    
    if (original == null) {
    logger.debug("Could not find original user Authentication object!");
    throw new AuthenticationCredentialsNotFoundException(messages.getMessage("SwitchUserFilter.noOriginalAuthentication",
    "Could not find original Authentication object"));
    }
    
    // get the source user details
    UserDetails originalUser = null;
    Object obj = original.getPrincipal();
    
    if ((obj != null) && obj instanceof UserDetails) {
    originalUser = (UserDetails) obj;
    }
    
    // publish event
    if (this.eventPublisher != null) {
    eventPublisher.publishEvent(new AuthenticationSwitchUserEvent(current, originalUser));
    }
    
    return original;
    }
    
    private UsernamePasswordAuthenticationToken createSwitchUserToken(final HttpServletRequest request, final UserDetails targetUser) {
    
    UsernamePasswordAuthenticationToken targetUserRequest;
    
    // grant an additional authority that contains the original Authentication object
    // which will be used to 'exit' from the current switched user.
    
    Authentication currentAuth;
    
    try {
    // SEC-1763. Check first if we are already switched.
    currentAuth = attemptExitUser(request);
    } catch (AuthenticationCredentialsNotFoundException e) {
    currentAuth = SecurityContextHolder.getContext().getAuthentication();
    }
    
    GrantedAuthority switchAuthority = new SwitchUserGrantedAuthority(SwitchUserFilter.ROLE_PREVIOUS_ADMINISTRATOR, currentAuth);
    
    // get the original authorities
    Collection<? extends GrantedAuthority> orig = targetUser.getAuthorities();
    
    // Allow subclasses to change the authorities to be granted
    if (switchUserAuthorityChanger != null) {
    orig = switchUserAuthorityChanger.modifyGrantedAuthorities(targetUser, currentAuth, orig);
    }
    
    // add the new switch user authority
    List<GrantedAuthority> newAuths = new ArrayList<GrantedAuthority>(orig);
    newAuths.add(switchAuthority);
    
    // create the new authentication token
    targetUserRequest = new UsernamePasswordAuthenticationToken(targetUser, targetUser.getPassword(), newAuths);
    
    // set details
    targetUserRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    
    return targetUserRequest;
    }
    
    private Authentication getSourceAuthentication(Authentication current) {
    Authentication original = null;
    
    // iterate over granted authorities and find the 'switch user' authority
    Collection<? extends GrantedAuthority> authorities = current.getAuthorities();
    
    for (GrantedAuthority auth : authorities) {
    // check for switch user type of authority
    if (auth instanceof SwitchUserGrantedAuthority) {
    original = ((SwitchUserGrantedAuthority) auth).getSource();
    logger.debug("Found original switch user granted authority [" + original + "]");
    }
    }
    
    return original;
    }
    }
    

The security constraint implementation will not be shown in this recipe as I plan to add a new one containing the details on how to define your own Spring permission evaluator and how to implement the stated requirements as well as how to expose the details of the currently logged in user for the client application to be consumed as a REST service.

To impersonate some user, we should access the endpoint /impersonate/login/?username=<some user> and to get back to the original user, if one is impersonating some user, /impersonate/logout.

Explanations

This implementation is all based on Spring security, namely the class SwitchUserFilter but exposed as a REST endpoint to be able to return a JSON object through a REST call and to be able to add specific security constraints implemented with something like hasPermission, instead of securing the URL for a specific role or roles with hasRole like access control rule.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s