User Impersonation with Spring Security SwitchUserFilter

A common requirement for secured applications is that admin / super users are able to login as any other user. For example, it may be helpful for a customer support analyst to access a system as if they were a specific real customer. The obvious way to do this is for the admin user to ask for the customer’s password or look it up in the password database. This is usually an unacceptable security compromise – no one should know a customer’s password except for the customer. And if the password database is implemented correctly it should be technically impossible for anyone – not even a system admin or DBA – to  discover a user’s password.

An alternative solution is to allow admin users to login with their own unique username and password and then allow them to impersonate any other user. After the admin user has logged in, they can enter the username of another user (no need for their password) and then view the application as if they had logged in as that user. Implementing user impersonation in this way also has the advantage that the system knows who has really logged in. If the system has an audit log, we can audit actions against the real admin user, rather than the impersonated user.

Implementing a user impersonation feature from scratch would be tricky and possibly introduce vulnerabilities to the system. Fortunately, this feature is available in Spring Security.

The following example is taken from the current release of the Spanners demo application, available on GitHub.

SwitchUserFilter

The starting point for user impersonation in Spring Security is the SwitchUserFilter. This Filter creates a URL that can be used to update the SecurityContext so that a different user is logged in. Here’s an example user switch URL:

http://localhost:8080/spanners-mvc/admin/impersonate?username=jones

The /admin/impersonate URL was configured by adding the following to the application’s spring-security-context.xml:

<http auto-config="true" disable-url-rewriting="true" use-expressions="true">

    <!-- Enable user switching - admin users may view the site as another user -->
    <custom-filter position="SWITCH_USER_FILTER" ref="switchUserProcessingFilter" />
</http>

<beans:bean id="switchUserProcessingFilter" class="org.springframework.security.web.authentication.switchuser.SwitchUserFilter">
    <beans:property name="userDetailsService" ref="userDetailsService"/>
    <beans:property name="switchUserUrl" value="/admin/impersonate"/>
    <beans:property name="targetUrl" value="/displaySpanners"/>
    <beans:property name="switchFailureUrl" value="/admin/switchUser"/>
</beans:bean>

The switchUserProcessingFilter is set up with the following settings:

  1. A reference to the userDetailsService bean, configured by Spring, is injected
  2. The switchUserUrl is set to /admin/impersonate. Any requests to /admin/impersonate will be handled by the SwitchUserFilter.
  3. The targetUrl is the first page shown on a successful user switch
  4. The switchFailureUrl is shown on failure to switch user. I prefer to have this go back to the switch user form (see below) rather than show a dedicated error page.

The filter is configured in addition to the filters configured automatically by Spring Security (using <http auto-config=”true”>) at the position ‘SWITCH_USER_FILTER‘.

Switch User form

With the above security configuration, it is possible for ADMIN users to type in a URL that switches them to another user’s profile. Rather than access the switch user URL directly though, it would be easier to submit a form.

Switch User Form

There’s no need for a Spring Controller to handle the POST submission of this form. It can just submit a GET request directly to the switch filter URL:

<form method="GET" action="<c:url value="/admin/impersonate"/>" class="form">
    <label for="usernameField">User name:</label>
    <input type="text" name="username" id="usernameField" />
    <input type="submit" value="Switch User" />
</form>

Securing the form and filter URL

If no additional security rules are configured then any user could access the user switch page or the processing filter. This would allow any user of the application to be able to impersonate any other user. Both the form page and the filter URL should be protected so that only users with ADMIN role can access them:

<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')" />

If any other user attempts to access the form or the processing filter URL, they’ll get an HTTP 403 Forbidden error. It is absolutely vital to secure the configured switchUserUrl in this way to prevent ordinary users from accessing this functionality.

Who am I? No, who am I really?

Once we’ve switched to another user using this mechanism, the Authentication object in the SecurityContext is that of the switched user. If you query the current user’s name, permissions or roles, you’ll get those of the switched user, not those of the ADMIN user who actually logged in. In some cases, we need to check details of the real user. These are stored as an additional GrantedAuthority of the switched user. An instance of SwitchUserGrantedAuthority signifies that the current user is being impersonated. The original ADMIN user can be retrieved via SwitchUserGrantedAuthority.getSource().

The SwitchUserGrantedAuthority always corresponds to a role named ‘ROLE_PREVIOUS_ADMINISTRATOR’, as defined in SwitchUserFilter. This allows us to easily grant access to ADMIN users even if they’re currently impersonating another user – like this:

<intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_PREVIOUS_ADMINISTRATOR')" />

Testing access rules

It’s worth adding unit tests around expected access rules. In particular, we should verify that non-admin users can never access the user switch page. This can easily be tested using the @WithMockUser annotation with the Spring MVC Test Framework, as detailed in a previous post. As an example, this test verifies that ADMIN users can access the restricted SwitchUserController URL, even if they’re currently impersonating a user with the EDITOR role.

@Test
@WithMockUser(roles={ROLE_PREVIOUS_ADMINISTRATOR, // User logged in as ADMIN...
					 ROLE_EDITOR}) //...but is currently viewing as an EDITOR
public void testAdminPathIsAvailableToAdminUserSwitchedToViewer() throws Exception {
	mockMvc.perform(get(SwitchUserController.CONTROLLER_URL))
			.andExpect(status().isOk());
}

 

12 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *