org.springframework.security.web.authentication.switchuser.SwitchUserFilter Maven / Gradle / Ivy
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.authentication.switchuser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
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.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;
import org.springframework.web.util.UrlPathHelper;
/**
* Switch User processing filter responsible for user context switching.
*
* This filter is similar to Unix 'su' however for Spring Security-managed web
* applications. A common use-case for this feature is the ability to allow
* higher-authority users (e.g. ROLE_ADMIN) to switch to a regular user (e.g. ROLE_USER).
*
* This filter assumes that the user performing the switch will be required to be logged
* in as normal (i.e. as a ROLE_ADMIN user). The user will then access a page/controller
* that enables the administrator to specify who they wish to become (see
* switchUserUrl
).
*
* Note: This URL will be required to have appropriate security constraints configured
* so that only users of that role can access it (e.g. ROLE_ADMIN).
*
* On a successful switch, the user's SecurityContext
will be updated to
* reflect the specified user and will also contain an additional
* {@link org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority}
* which contains the original user. Before switching, a check will be made on whether the
* user is already currently switched, and any current switch will be exited to prevent
* "nested" switches.
*
* To 'exit' from a user context, the user needs to access a URL (see
* exitUserUrl
) that will switch back to the original user as identified by
* the ROLE_PREVIOUS_ADMINISTRATOR
.
*
* To configure the Switch User Processing Filter, create a bean definition for the Switch
* User processing filter and add to the filterChainProxy. Note that the filter must come
* after the FilterSecurityInteceptor in the chain, in order to apply the
* correct constraints to the switchUserUrl. Example:
*
*
* <bean id="switchUserProcessingFilter" class="org.springframework.security.web.authentication.switchuser.SwitchUserFilter">
* <property name="userDetailsService" ref="userDetailsService" />
* <property name="switchUserUrl" value="/login/impersonate" />
* <property name="exitUserUrl" value="/logout/impersonate" />
* <property name="targetUrl" value="/index.jsp" />
* </bean>
*
*
* @author Mark St.Godard
* @see SwitchUserGrantedAuthority
*/
public class SwitchUserFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
public static final String SPRING_SECURITY_SWITCH_USERNAME_KEY = "username";
public static final String ROLE_PREVIOUS_ADMINISTRATOR = "ROLE_PREVIOUS_ADMINISTRATOR";
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private ApplicationEventPublisher eventPublisher;
private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private RequestMatcher exitUserMatcher = createMatcher("/logout/impersonate");
private RequestMatcher switchUserMatcher = createMatcher("/login/impersonate");
private String targetUrl;
private String switchFailureUrl;
private String usernameParameter = SPRING_SECURITY_SWITCH_USERNAME_KEY;
private String switchAuthorityRole = ROLE_PREVIOUS_ADMINISTRATOR;
private SwitchUserAuthorityChanger switchUserAuthorityChanger;
private UserDetailsService userDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
private AuthenticationSuccessHandler successHandler;
private AuthenticationFailureHandler failureHandler;
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
@Override
public void afterPropertiesSet() {
Assert.notNull(this.userDetailsService, "userDetailsService must be specified");
Assert.isTrue(this.successHandler != null || this.targetUrl != null,
"You must set either a successHandler or the targetUrl");
if (this.targetUrl != null) {
Assert.isNull(this.successHandler, "You cannot set both successHandler and targetUrl");
this.successHandler = new SimpleUrlAuthenticationSuccessHandler(this.targetUrl);
}
if (this.failureHandler == null) {
this.failureHandler = (this.switchFailureUrl != null)
? new SimpleUrlAuthenticationFailureHandler(this.switchFailureUrl)
: new SimpleUrlAuthenticationFailureHandler();
}
else {
Assert.isNull(this.switchFailureUrl, "You cannot set both a switchFailureUrl and a failureHandler");
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// check for switch or exit request
if (requiresSwitchUser(request)) {
// if set, attempt switch and store original
try {
Authentication targetUser = attemptSwitchUser(request);
// update the current context to the new target user
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(targetUser);
this.securityContextHolderStrategy.setContext(context);
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", targetUser));
this.securityContextRepository.saveContext(context, request, response);
// redirect to target url
this.successHandler.onAuthenticationSuccess(request, response, targetUser);
}
catch (AuthenticationException ex) {
this.logger.debug("Failed to switch user", ex);
this.failureHandler.onAuthenticationFailure(request, response, ex);
}
return;
}
if (requiresExitUser(request)) {
// get the original authentication object (if exists)
Authentication originalUser = attemptExitUser(request);
// update the current context back to the original user
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(originalUser);
this.securityContextHolderStrategy.setContext(context);
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", originalUser));
this.securityContextRepository.saveContext(context, request, response);
// redirect to target url
this.successHandler.onAuthenticationSuccess(request, response, originalUser);
return;
}
this.logger.trace(LogMessage.format("Did not attempt to switch user since request did not match [%s] or [%s]",
this.switchUserMatcher, this.exitUserMatcher));
chain.doFilter(request, response);
}
/**
* Attempt to switch to another user. If the user does not exist or is not active,
* return null.
* @return The new Authentication
request if successfully switched to
* another user, null
otherwise.
* @throws UsernameNotFoundException If the target user is not found.
* @throws LockedException if the account is locked.
* @throws DisabledException If the target user is disabled.
* @throws AccountExpiredException If the target user account is expired.
* @throws CredentialsExpiredException If the target user credentials are expired.
*/
protected Authentication attemptSwitchUser(HttpServletRequest request) throws AuthenticationException {
UsernamePasswordAuthenticationToken targetUserRequest;
String username = request.getParameter(this.usernameParameter);
username = (username != null) ? username : "";
this.logger.debug(LogMessage.format("Attempting to switch to user [%s]", username));
UserDetails targetUser = this.userDetailsService.loadUserByUsername(username);
this.userDetailsChecker.check(targetUser);
// OK, create the switch user token
targetUserRequest = createSwitchUserToken(request, targetUser);
// publish event
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new AuthenticationSwitchUserEvent(
this.securityContextHolderStrategy.getContext().getAuthentication(), targetUser));
}
return targetUserRequest;
}
/**
* Attempt to exit from an already switched user.
* @param request The http servlet request
* @return The original Authentication
object or null
* otherwise.
* @throws AuthenticationCredentialsNotFoundException If no
* Authentication
associated with this request.
*/
protected Authentication attemptExitUser(HttpServletRequest request)
throws AuthenticationCredentialsNotFoundException {
// need to check to see if the current user has a SwitchUserGrantedAuthority
Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
if (current == null) {
throw new AuthenticationCredentialsNotFoundException(this.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) {
this.logger.debug("Failed to find original user");
throw new AuthenticationCredentialsNotFoundException(this.messages
.getMessage("SwitchUserFilter.noOriginalAuthentication", "Failed to find original user"));
}
// 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) {
this.eventPublisher.publishEvent(new AuthenticationSwitchUserEvent(current, originalUser));
}
return original;
}
/**
* Create a switch user token that contains an additional GrantedAuthority
* that contains the original Authentication
object.
* @param request The http servlet request.
* @param targetUser The target user
* @return The authentication token
*
* @see SwitchUserGrantedAuthority
*/
private UsernamePasswordAuthenticationToken createSwitchUserToken(HttpServletRequest request,
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 currentAuthentication = getCurrentAuthentication(request);
GrantedAuthority switchAuthority = new SwitchUserGrantedAuthority(this.switchAuthorityRole,
currentAuthentication);
// get the original authorities
Collection orig = targetUser.getAuthorities();
// Allow subclasses to change the authorities to be granted
if (this.switchUserAuthorityChanger != null) {
orig = this.switchUserAuthorityChanger.modifyGrantedAuthorities(targetUser, currentAuthentication, orig);
}
// add the new switch user authority
List newAuths = new ArrayList<>(orig);
newAuths.add(switchAuthority);
// create the new authentication token
targetUserRequest = UsernamePasswordAuthenticationToken.authenticated(targetUser, targetUser.getPassword(),
newAuths);
// set details
targetUserRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
return targetUserRequest;
}
private Authentication getCurrentAuthentication(HttpServletRequest request) {
try {
// SEC-1763. Check first if we are already switched.
return attemptExitUser(request);
}
catch (AuthenticationCredentialsNotFoundException ex) {
return this.securityContextHolderStrategy.getContext().getAuthentication();
}
}
/**
* Find the original Authentication
object from the current user's
* granted authorities. A successfully switched user should have a
* SwitchUserGrantedAuthority
that contains the original source user
* Authentication
object.
* @param current The current Authentication
object
* @return The source user Authentication
object or null
* otherwise.
*/
private Authentication getSourceAuthentication(Authentication current) {
Authentication original = null;
// iterate over granted authorities and find the 'switch user' authority
Collection authorities = current.getAuthorities();
for (GrantedAuthority auth : authorities) {
// check for switch user type of authority
if (auth instanceof SwitchUserGrantedAuthority) {
original = ((SwitchUserGrantedAuthority) auth).getSource();
this.logger.debug(LogMessage.format("Found original switch user granted authority [%s]", original));
}
}
return original;
}
/**
* Checks the request URI for the presence of exitUserUrl.
* @param request The http servlet request
* @return true
if the request requires a exit user, false
* otherwise.
*
* @see SwitchUserFilter#setExitUserUrl(String)
*/
protected boolean requiresExitUser(HttpServletRequest request) {
return this.exitUserMatcher.matches(request);
}
/**
* Checks the request URI for the presence of switchUserUrl.
* @param request The http servlet request
* @return true
if the request requires a switch, false
* otherwise.
*
* @see SwitchUserFilter#setSwitchUserUrl(String)
*/
protected boolean requiresSwitchUser(HttpServletRequest request) {
return this.switchUserMatcher.matches(request);
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) throws BeansException {
this.eventPublisher = eventPublisher;
}
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
@Override
public void setMessageSource(MessageSource messageSource) {
Assert.notNull(messageSource, "messageSource cannot be null");
this.messages = new MessageSourceAccessor(messageSource);
}
/**
* Sets the authentication data access object.
* @param userDetailsService The UserDetailsService which will be used to
* load information for the user that is being switched to.
*/
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
/**
* Set the URL to respond to exit user processing. This is a shortcut for
* {@link #setExitUserMatcher(RequestMatcher)}.
* @param exitUserUrl The exit user URL.
*/
public void setExitUserUrl(String exitUserUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(exitUserUrl),
"exitUserUrl cannot be empty and must be a valid redirect URL");
this.exitUserMatcher = createMatcher(exitUserUrl);
}
/**
* Set the matcher to respond to exit user processing.
* @param exitUserMatcher The exit matcher to use.
*/
public void setExitUserMatcher(RequestMatcher exitUserMatcher) {
Assert.notNull(exitUserMatcher, "exitUserMatcher cannot be null");
this.exitUserMatcher = exitUserMatcher;
}
/**
* Set the URL to respond to switch user processing. This is a shortcut for
* {@link #setSwitchUserMatcher(RequestMatcher)}
* @param switchUserUrl The switch user URL.
*/
public void setSwitchUserUrl(String switchUserUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(switchUserUrl),
"switchUserUrl cannot be empty and must be a valid redirect URL");
this.switchUserMatcher = createMatcher(switchUserUrl);
}
/**
* Set the matcher to respond to switch user processing.
* @param switchUserMatcher The switch user matcher.
*/
public void setSwitchUserMatcher(RequestMatcher switchUserMatcher) {
Assert.notNull(switchUserMatcher, "switchUserMatcher cannot be null");
this.switchUserMatcher = switchUserMatcher;
}
/**
* Sets the URL to go to after a successful switch / exit user request. Use
* {@link #setSuccessHandler(AuthenticationSuccessHandler) setSuccessHandler} instead
* if you need more customized behaviour.
* @param targetUrl The target url.
*/
public void setTargetUrl(String targetUrl) {
this.targetUrl = targetUrl;
}
/**
* Used to define custom behaviour on a successful switch or exit user.
*
* Can be used instead of setting targetUrl.
*/
public void setSuccessHandler(AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.successHandler = successHandler;
}
/**
* Sets the URL to which a user should be redirected if the switch fails. For example,
* this might happen because the account they are attempting to switch to is invalid
* (the user doesn't exist, account is locked etc).
*
* If not set, an error message will be written to the response.
*
* Use {@link #setFailureHandler(AuthenticationFailureHandler) failureHandler} instead
* if you need more customized behaviour.
* @param switchFailureUrl the url to redirect to.
*/
public void setSwitchFailureUrl(String switchFailureUrl) {
Assert.isTrue(UrlUtils.isValidRedirectUrl(switchFailureUrl), "switchFailureUrl must be a valid redirect URL");
this.switchFailureUrl = switchFailureUrl;
}
/**
* Used to define custom behaviour when a switch fails.
*
* Can be used instead of setting switchFailureUrl.
*/
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
}
/**
* @param switchUserAuthorityChanger to use to fine-tune the authorities granted to
* subclasses (may be null if SwitchUserFilter should not fine-tune the authorities)
*/
public void setSwitchUserAuthorityChanger(SwitchUserAuthorityChanger switchUserAuthorityChanger) {
this.switchUserAuthorityChanger = switchUserAuthorityChanger;
}
/**
* Sets the {@link UserDetailsChecker} that is called on the target user whenever the
* user is switched.
* @param userDetailsChecker the {@link UserDetailsChecker} that checks the status of
* the user that is being switched to. Defaults to
* {@link AccountStatusUserDetailsChecker}.
*/
public void setUserDetailsChecker(UserDetailsChecker userDetailsChecker) {
this.userDetailsChecker = userDetailsChecker;
}
/**
* Allows the parameter containing the username to be customized.
* @param usernameParameter the parameter name. Defaults to {@code username}
*/
public void setUsernameParameter(String usernameParameter) {
this.usernameParameter = usernameParameter;
}
/**
* Allows the role of the switchAuthority to be customized.
* @param switchAuthorityRole the role name. Defaults to
* {@link #ROLE_PREVIOUS_ADMINISTRATOR}
*/
public void setSwitchAuthorityRole(String switchAuthorityRole) {
Assert.notNull(switchAuthorityRole, "switchAuthorityRole cannot be null");
this.switchAuthorityRole = switchAuthorityRole;
}
/**
* Sets the {@link SecurityContextHolderStrategy} to use. The default action is to use
* the {@link SecurityContextHolderStrategy} stored in {@link SecurityContextHolder}.
*
* @since 5.8
*/
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
/**
* Sets the {@link SecurityContextRepository} to save the {@link SecurityContext} on
* switch user success. The default is
* {@link RequestAttributeSecurityContextRepository}.
* @param securityContextRepository the {@link SecurityContextRepository} to use.
* Cannot be null.
* @since 5.7.7
*/
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
this.securityContextRepository = securityContextRepository;
}
private static RequestMatcher createMatcher(String pattern) {
return new AntPathRequestMatcher(pattern, "POST", true, new UrlPathHelper());
}
}