/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * licenses this file to you 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 the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>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.apereo.portal.spring.security.preauth; import java.io.IOException; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.UUID; 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.HttpSession; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apereo.portal.PortalException; import org.apereo.portal.layout.profile.ProfileSelectionEvent; import org.apereo.portal.portlets.swapper.IdentitySwapperPrincipal; import org.apereo.portal.portlets.swapper.IdentitySwapperSecurityContext; import org.apereo.portal.security.IPerson; import org.apereo.portal.security.IPersonManager; import org.apereo.portal.security.IdentitySwapperManager; import org.apereo.portal.security.mvc.LoginController; import org.apereo.portal.services.Authentication; import org.apereo.portal.spring.security.PortalPersonUserDetails; import org.apereo.portal.utils.ResourceLoader; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; /** * PortalPreAuthenticatedProcessingFilter enables Spring Security pre-authentication in uPortal by * returning the current IPerson object as the user details. * * <p>At login, fires ProfileSelectionEvent representing any runtime request for a profile selection * (as in, profile request parameter or target profile indicated by the swapper manager). * */ public class PortalPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter { protected final Log swapperLog = LogFactory.getLog("org.jasig.portal.portlets.swapper"); private String loginPath = "/Login"; private String logoutPath = "/Logout"; protected HashMap<String, String> credentialTokens; protected HashMap<String, String> principalTokens; protected Authentication authenticationService = null; private IPersonManager personManager; private IdentitySwapperManager identitySwapperManager; private ApplicationEventPublisher eventPublisher; private boolean clearSecurityContextPriorToPortalAuthentication = true; //default @Autowired public void setIdentitySwapperManager(IdentitySwapperManager identitySwapperManager) { this.identitySwapperManager = identitySwapperManager; } @Autowired public void setPersonManager(IPersonManager personManager) { this.personManager = personManager; } @Autowired public void setAuthenticationService(Authentication authenticationService) { this.authenticationService = authenticationService; } public void setClearSecurityContextPriorToPortalAuthentication(boolean b) { this.clearSecurityContextPriorToPortalAuthentication = b; } @Override public void afterPropertiesSet() { super.afterPropertiesSet(); this.credentialTokens = new HashMap<String, String>(1); this.principalTokens = new HashMap<String, String>(1); this.retrieveCredentialAndPrincipalTokensFromPropertiesFile(); } /** * Set the path to the portal's local login servlet. * * @param loginPath */ public void setLoginPath(String loginPath) { this.loginPath = loginPath; } /** * Set the path to the portal's local logout servlet. * * @param logoutPath */ public void setLogoutPath(String logoutPath) { this.logoutPath = logoutPath; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // Set up some DEBUG logging for performance troubleshooting final long timestamp = System.currentTimeMillis(); UUID uuid = null; // Tagging with a UUID (instead of username) because username changes in the /Login process if (logger.isDebugEnabled()) { uuid = UUID.randomUUID(); final HttpServletRequest httpr = (HttpServletRequest) request; logger.debug( "STARTING [" + uuid.toString() + "] for URI=" + httpr.getRequestURI() + " #milestone"); } HttpServletRequest httpServletRequest = (HttpServletRequest) request; String currentPath = httpServletRequest.getServletPath(); /** * Override the base class's main filter method to bypass this filter if we're currently at * the login servlet. Since that servlet sets up the user session and authentication, we * need it to run before this filter is useful. */ if (loginPath.equals(currentPath)) { final org.springframework.security.core.Authentication originalAuthentication = SecurityContextHolder.getContext().getAuthentication(); if (this.clearSecurityContextPriorToPortalAuthentication) { SecurityContextHolder.clearContext(); } this.logForLoginPath(currentPath); this.doPortalAuthentication((HttpServletRequest) request, originalAuthentication); chain.doFilter(request, response); } else if (logoutPath.equals(currentPath)) { SecurityContextHolder.clearContext(); this.logForLogoutPath(currentPath); chain.doFilter(request, response); } // otherwise, call the base class logic else { this.logForNonLoginOrLogoutPath(currentPath); super.doFilter(request, response, chain); } if (logger.isDebugEnabled()) { final HttpServletRequest httpr = (HttpServletRequest) request; logger.debug( "FINISHED [" + uuid.toString() + "] for URI=" + httpr.getRequestURI() + " in " + Long.toString(System.currentTimeMillis() - timestamp) + "ms #milestone"); } } @Override protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { // if there's no session, the user hasn't yet visited the login servlet and we should just give up HttpSession session = request.getSession(false); if (session == null) { return null; } // otherwise, use the person's current SecurityContext as the credentials final IPerson person = personManager.getPerson(request); return person.getSecurityContext(); } @Override protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { // if there's no session, the user hasn't yet visited the login servlet and we should just give up HttpSession session = request.getSession(false); if (session == null) { return null; } // otherwise, use the current IPerson as the UserDetails final IPerson person = personManager.getPerson(request); final UserDetails details = new PortalPersonUserDetails(person); return details; } private void doPortalAuthentication( final HttpServletRequest request, final org.springframework.security.core.Authentication originalAuthentication) { IdentitySwapHelper identitySwapHelper = null; final String requestedSessionId = request.getRequestedSessionId(); if (request.isRequestedSessionIdValid()) { if (logger.isDebugEnabled()) { logger.debug( "doPortalAuthentication for valid requested session id " + requestedSessionId); } identitySwapHelper = getIdentitySwapDataAndInvalidateSession(request, originalAuthentication); } else { if (logger.isTraceEnabled()) { logger.trace( "Requested session id " + requestedSessionId + " was not valid " + "so no attempt to apply swapping rules."); } } HttpSession s = request.getSession(true); IPerson person = null; try { final HashMap<String, String> principals; final HashMap<String, String> credentials; person = personManager.getPerson(request); if (identitySwapHelper != null && identitySwapHelper.isSwapOrUnswapRequest()) { this.handleIdentitySwap(person, s, identitySwapHelper); principals = new HashMap<String, String>(); credentials = new HashMap<String, String>(); } //Norm authN path else { // WE grab all of the principals and credentials from the request and load // them into their respective HashMaps. principals = getPropertyFromRequest(principalTokens, request); credentials = getPropertyFromRequest(credentialTokens, request); } // Attempt to authenticate using the incoming request authenticationService.authenticate(request, principals, credentials, person); } catch (Exception e) { // Log the exception logger.error("Exception authenticating the request", e); // Reset everything request.getSession(false).invalidate(); // Add the authentication failure request.getSession(true).setAttribute(LoginController.AUTH_ERROR_KEY, Boolean.TRUE); } this.publishProfileSelectionEvent(person, request, identitySwapHelper); } /** * Helper inner class for encapsulating logic for determining whether or not the request is for * an identity "swap" or "unswap", and determining the "swap from" and "swap to" values. */ class IdentitySwapHelper { private String originalUsername; private String personName; private String targetProfile; private String targetUsername; private org.springframework.security.core.Authentication originalAuthenticationForSwap; private org.springframework.security.core.Authentication originalAuthenticationForUnswap; IdentitySwapHelper(final HttpSession s, final String personName) { // must pull out session data during creation because session may later be invalidated this.originalAuthenticationForUnswap = identitySwapperManager.getOriginalAuthentication(s); this.originalUsername = identitySwapperManager.getOriginalUsername(s); this.personName = personName; this.targetUsername = identitySwapperManager.getTargetUsername(s); this.targetProfile = identitySwapperManager.getTargetProfile(s); } public boolean isSwapRequest() { return this.originalUsername == null && this.targetUsername != null; } public boolean isUnswapRequest() { return this.originalUsername != null; } public boolean isSwapOrUnswapRequest() { return this.isSwapRequest() || this.isUnswapRequest(); } public org.springframework.security.core.Authentication getOriginalAuthenticationForSwap() { return this.originalAuthenticationForSwap; } public org.springframework.security.core.Authentication getOriginalAuthenticationForUnswap() { return this.originalAuthenticationForUnswap; } public String getSwapFromUid() { if (this.isSwapRequest()) { return this.personName; } if (this.isUnswapRequest()) { return this.targetUsername; } return null; } public String getSwapToUid() { if (this.isSwapRequest()) { return this.targetUsername; } if (this.isUnswapRequest()) { return this.originalUsername; } return null; } public String getTargetProfile() { return this.targetProfile; } public void setOriginalAuthenticationForSwap( final org.springframework.security.core.Authentication auth) { this.originalAuthenticationForSwap = auth; } } private IdentitySwapHelper getIdentitySwapDataAndInvalidateSession( final HttpServletRequest request, final org.springframework.security.core.Authentication originalAuth) { IdentitySwapHelper identitySwapHelper = null; try { HttpSession s = request.getSession(false); if (s != null) { final IPerson person = personManager.getPerson(request); identitySwapHelper = new IdentitySwapHelper(s, person.getName()); if (identitySwapHelper.isSwapRequest()) { identitySwapHelper.setOriginalAuthenticationForSwap(originalAuth); } if (logger.isDebugEnabled()) { logger.debug("Invalidating the impersonated session in un-swapping."); } s.invalidate(); } } catch (IllegalStateException ise) { // ISE indicates session was already invalidated. // This is fine. This servlet trying to guarantee that the session has been invalidated; // it doesn't have to insist that it is the one that invalidated it. if (logger.isTraceEnabled()) { logger.trace( "LoginServlet attempted to invalidate an already invalid session.", ise); } } return identitySwapHelper; } private void handleIdentitySwap( final IPerson person, final HttpSession session, final IdentitySwapHelper identitySwapHelper) { String msgFormat; if (identitySwapHelper.isSwapRequest()) { msgFormat = "Swapping identity for '%s' to '%s'"; this.identitySwapperManager.setOriginalUser( session, identitySwapHelper.getSwapFromUid(), identitySwapHelper.getSwapToUid(), identitySwapHelper.getOriginalAuthenticationForSwap()); } else { msgFormat = "Reverting swapped identity from '%s' to '%s'"; if (identitySwapHelper.getOriginalAuthenticationForUnswap() != null) { SecurityContextHolder.getContext() .setAuthentication(identitySwapHelper.getOriginalAuthenticationForUnswap()); } } person.setUserName(identitySwapHelper.getSwapToUid()); final String msg = String.format( msgFormat, identitySwapHelper.getSwapFromUid(), identitySwapHelper.getSwapToUid()); swapperLog.warn(msg); //Setup the custom security context final IdentitySwapperPrincipal identitySwapperPrincipal = new IdentitySwapperPrincipal(person); final IdentitySwapperSecurityContext identitySwapperSecurityContext = new IdentitySwapperSecurityContext(identitySwapperPrincipal); person.setSecurityContext(identitySwapperSecurityContext); } private void publishProfileSelectionEvent( final IPerson person, final HttpServletRequest request, final IdentitySwapHelper identitySwapHelper) { final String requestedProfile = request.getParameter(LoginController.REQUESTED_PROFILE_KEY); if (requestedProfile != null) { final ProfileSelectionEvent event = new ProfileSelectionEvent(this, requestedProfile, person, request); this.publishProfileSelectionEvent(event); } else if (identitySwapHelper != null && identitySwapHelper.isSwapRequest()) { final ProfileSelectionEvent event = new ProfileSelectionEvent( this, identitySwapHelper.getTargetProfile(), person, request); this.publishProfileSelectionEvent(event); } else { if (logger.isTraceEnabled()) { logger.trace( "No requested or swapper profile requested so no profile selection event."); } } } private void publishProfileSelectionEvent(final ProfileSelectionEvent event) { try { this.eventPublisher.publishEvent(event); } catch (final Exception exceptionFiringProfileSelection) { // failing to swap as the desired profile selection is bad, // but preventing login entirely is worse. Log the exception and continue. logger.error( "Exception on firing profile selection event " + event, exceptionFiringProfileSelection); } } private void logForLoginPath(final String currentPath) { if (logger.isDebugEnabled()) { logger.debug( "Path [" + currentPath + "] is loginPath, so cleared security context" + " so we can re-establish it once the new session is established."); } } private void logForLogoutPath(final String currentPath) { if (logger.isDebugEnabled()) { logger.debug( "Path [" + currentPath + "] is logoutPath, so cleared security context" + " so can re-establish it once the new session is established."); } } private void logForNonLoginOrLogoutPath(final String currentPath) { if (logger.isTraceEnabled()) { logger.trace( "Path [" + currentPath + "] is neither a login nor a logout path," + " so no uPortal-custom filtering."); } } private void retrieveCredentialAndPrincipalTokensFromPropertiesFile() { try { String key; // We retrieve the tokens representing the credential and principal // parameters from the security properties file. Properties props = ResourceLoader.getResourceAsProperties( getClass(), "/properties/security.properties"); Enumeration<?> propNames = props.propertyNames(); while (propNames.hasMoreElements()) { String propName = (String) propNames.nextElement(); String propValue = props.getProperty(propName); if (propName.startsWith("credentialToken.")) { key = propName.substring(16); this.credentialTokens.put(key, propValue); } if (propName.startsWith("principalToken.")) { key = propName.substring(15); this.principalTokens.put(key, propValue); } } } catch (PortalException pe) { logger.error("LoginServlet::static ", pe); } catch (IOException ioe) { logger.error("LoginServlet::static ", ioe); } } /** * Get the values represented by each token from the request and load them into a HashMap that * is returned. * * @param tokens * @param request * @return HashMap of properties */ private HashMap<String, String> getPropertyFromRequest( HashMap<String, String> tokens, HttpServletRequest request) { // Iterate through all of the other property keys looking for the first property // named like propname that has a value in the request HashMap<String, String> retHash = new HashMap<String, String>(1); for (Map.Entry<String, String> entry : tokens.entrySet()) { String contextName = entry.getKey(); String parmName = entry.getValue(); String parmValue = null; if (request.getAttribute(parmName) != null) { // Upstream components (like servlet filters) may supply information // for the authentication process using request attributes. try { parmValue = (String) request.getAttribute(parmName); } catch (ClassCastException cce) { String msg = "The request attribute '" + parmName + "' must be a String."; throw new RuntimeException(msg, cce); } } else { // If a configured parameter isn't provided by a request attribute, // check request parameters (i.e. querystring, form fields). parmValue = request.getParameter(parmName); } // null value causes exception in context.authentication // alternately we could just not set parm if value is null if ("password".equals(parmName)) { // make sure we don't trim passwords, since they might have // leading or trailing spaces parmValue = (parmValue == null ? "" : parmValue); } else { parmValue = (parmValue == null ? "" : parmValue).trim(); } // The relationship between the way the properties are stored and the way // the subcontexts are named has to be closely looked at to make this work. // The keys are either "root" or the subcontext name that follows "root.". As // as example, the contexts ["root", "root.simple", "root.cas"] are represented // as ["root", "simple", "cas"]. String key = (contextName.startsWith("root.") ? contextName.substring(5) : contextName); retHash.put(key, parmValue); } return (retHash); } @Override public void setApplicationEventPublisher( ApplicationEventPublisher anApplicationEventPublisher) { super.setApplicationEventPublisher(anApplicationEventPublisher); this.eventPublisher = anApplicationEventPublisher; } /** * Convenience method for sub-classes to access the event publisher without having to override * this class implementation of setApplicationEventPublisher (which this class had to override * in its parent class because that parent class failed to expose a getter method like this!) * * @return the Spring application event publisher. */ protected final ApplicationEventPublisher getApplicationEventPublisher() { return this.eventPublisher; } }