/* * 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 * * http://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 javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.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.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.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.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.util.UrlUtils; import org.springframework.util.Assert; import org.springframework.web.filter.GenericFilterBean; /** * Switch User processing filter responsible for user context switching. * <p> * 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). * <p> * 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 * <code>switchUserUrl</code>). * <p> * <b>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).</b> * <p> * On a successful switch, the user's <code>SecurityContext</code> 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. * <p> * To 'exit' from a user context, the user needs to access a URL (see * <code>exitUserUrl</code>) that will switch back to the original user as identified by * the <code>ROLE_PREVIOUS_ADMINISTRATOR</code>. * <p> * 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 * <b>after</b> the <tt>FilterSecurityInteceptor</tt> in the chain, in order to apply the * correct constraints to the <tt>switchUserUrl</tt>. Example: * * <pre> * <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> * </pre> * * @author Mark St.Godard * * @see SwitchUserGrantedAuthority */ public class SwitchUserFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware { // ~ Static fields/initializers // ===================================================================================== public static final String SPRING_SECURITY_SWITCH_USERNAME_KEY = "username"; public static final String ROLE_PREVIOUS_ADMINISTRATOR = "ROLE_PREVIOUS_ADMINISTRATOR"; // ~ Instance fields // ================================================================================================ private ApplicationEventPublisher eventPublisher; private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource(); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private String exitUserUrl = "/logout/impersonate"; private String switchUserUrl = "/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; // ~ Methods // ======================================================================================================== @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() : new SimpleUrlAuthenticationFailureHandler(this.switchFailureUrl); } else { Assert.isNull(this.switchFailureUrl, "You cannot set both a switchFailureUrl and a failureHandler"); } } public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 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 SecurityContextHolder.getContext().setAuthentication(targetUser); // redirect to target url this.successHandler.onAuthenticationSuccess(request, response, targetUser); } catch (AuthenticationException e) { this.logger.debug("Switch User failed", e); this.failureHandler.onAuthenticationFailure(request, response, e); } return; } else if (requiresExitUser(request)) { // get the original authentication object (if exists) Authentication originalUser = attemptExitUser(request); // update the current context back to the original user SecurityContextHolder.getContext().setAuthentication(originalUser); // redirect to target url this.successHandler.onAuthenticationSuccess(request, response, originalUser); return; } 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 <code>Authentication</code> request if successfully switched to * another user, <code>null</code> 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); if (username == null) { username = ""; } if (this.logger.isDebugEnabled()) { this.logger.debug("Attempt to switch to user [" + username + "]"); } UserDetails targetUser = this.userDetailsService.loadUserByUsername(username); this.userDetailsChecker.check(targetUser); // OK, create the switch user token targetUserRequest = createSwitchUserToken(request, targetUser); if (this.logger.isDebugEnabled()) { this.logger.debug("Switch User Token [" + targetUserRequest + "]"); } // publish event if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new AuthenticationSwitchUserEvent( SecurityContextHolder.getContext().getAuthentication(), targetUser)); } return targetUserRequest; } /** * Attempt to exit from an already switched user. * * @param request The http servlet request * * @return The original <code>Authentication</code> object or <code>null</code> * otherwise. * * @throws AuthenticationCredentialsNotFoundException If no * <code>Authentication</code> 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 = SecurityContextHolder.getContext().getAuthentication(); if (null == current) { 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("Could not find original user Authentication object!"); throw new AuthenticationCredentialsNotFoundException( this.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) { this.eventPublisher.publishEvent( new AuthenticationSwitchUserEvent(current, originalUser)); } return original; } /** * Create a switch user token that contains an additional <tt>GrantedAuthority</tt> * that contains the original <code>Authentication</code> 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 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( this.switchAuthorityRole, currentAuth); // get the original authorities Collection<? extends GrantedAuthority> orig = targetUser.getAuthorities(); // Allow subclasses to change the authorities to be granted if (this.switchUserAuthorityChanger != null) { orig = this.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(this.authenticationDetailsSource.buildDetails(request)); return targetUserRequest; } /** * Find the original <code>Authentication</code> object from the current user's * granted authorities. A successfully switched user should have a * <code>SwitchUserGrantedAuthority</code> that contains the original source user * <code>Authentication</code> object. * * @param current The current <code>Authentication</code> object * * @return The source user <code>Authentication</code> object or <code>null</code> * otherwise. */ 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(); this.logger.debug("Found original switch user granted authority [" + original + "]"); } } return original; } /** * Checks the request URI for the presence of <tt>exitUserUrl</tt>. * * @param request The http servlet request * * @return <code>true</code> if the request requires a exit user, <code>false</code> * otherwise. * * @see SwitchUserFilter#exitUserUrl */ protected boolean requiresExitUser(HttpServletRequest request) { String uri = stripUri(request); return uri.endsWith(request.getContextPath() + this.exitUserUrl); } /** * Checks the request URI for the presence of <tt>switchUserUrl</tt>. * * @param request The http servlet request * * @return <code>true</code> if the request requires a switch, <code>false</code> * otherwise. * * @see SwitchUserFilter#switchUserUrl */ protected boolean requiresSwitchUser(HttpServletRequest request) { String uri = stripUri(request); return uri.endsWith(request.getContextPath() + this.switchUserUrl); } public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) throws BeansException { this.eventPublisher = eventPublisher; } public void setAuthenticationDetailsSource( AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) { Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required"); this.authenticationDetailsSource = authenticationDetailsSource; } 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 <tt>UserDetailService</tt> 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. * * @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.exitUserUrl = exitUserUrl; } /** * Set the URL to respond to switch user processing. * * @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.switchUserUrl = switchUserUrl; } /** * 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. * <p> * Can be used instead of setting <tt>targetUrl</tt>. */ 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). * <p> * If not set, an error message will be written to the response. * <p> * 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. * <p> * Can be used instead of setting <tt>switchFailureUrl</tt>. */ 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; } 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; } /** * Strips any content after the ';' in the request URI * * @param request The http request * * @return The stripped uri */ private String stripUri(HttpServletRequest request) { String uri = request.getRequestURI(); int idx = uri.indexOf(';'); if (idx > 0) { uri = uri.substring(0, idx); } return uri; } }