/** * 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.security.mvc; import java.io.IOException; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; 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.events.IPortalAuthEventFactory; import org.apereo.portal.security.IPerson; import org.apereo.portal.security.IPersonManager; import org.apereo.portal.security.ISecurityContext; import org.apereo.portal.security.IdentitySwapperManager; import org.apereo.portal.utils.ResourceLoader; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * Simple servlet to handle user logout. When a user logs out, their session gets invalidated and * they are returned to the guest page. * */ @Controller @RequestMapping("/Logout") public class LogoutController implements InitializingBean { private static final Log log = LogFactory.getLog(LogoutController.class); private Map<String, String> redirectMap; private IPortalAuthEventFactory portalEventFactory; private IPersonManager personManager; private IdentitySwapperManager identitySwapperManager; @Autowired public void setIdentitySwapperManager(IdentitySwapperManager identitySwapperManager) { this.identitySwapperManager = identitySwapperManager; } @Autowired public void setPersonManager(IPersonManager personManager) { this.personManager = personManager; } @Autowired public void setPortalEventFactory(IPortalAuthEventFactory portalEventFactory) { this.portalEventFactory = portalEventFactory; } @Override public void afterPropertiesSet() throws Exception { final Map<String, String> rdHash = new HashMap<String, String>(1); try { // We retrieve the redirect strings for each context // from the security properties file. String key; final Properties props = ResourceLoader.getResourceAsProperties( LogoutController.class, "/properties/security.properties"); final Enumeration propNames = props.propertyNames(); while (propNames.hasMoreElements()) { final String propName = (String) propNames.nextElement(); final String propValue = props.getProperty(propName); if (propName.startsWith("logoutRedirect.")) { key = propName.substring(15); key = key.startsWith("root.") ? key.substring(5) : key; if (log.isDebugEnabled()) { log.debug("Redirect key=" + key + ", value=" + propValue); } rdHash.put(key, propValue); } } } catch (final PortalException pe) { log.error("Failed to load logout redirect URLs", pe); } catch (final IOException ioe) { log.error("Failed to load logout redirect URLs", ioe); } this.redirectMap = rdHash; } /** * Process the incoming request and response. * * @param request HttpServletRequest object * @param response HttpServletResponse object * @throws ServletException * @throws IOException */ @RequestMapping public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String redirect = this.getRedirectionUrl(request); final HttpSession session = request.getSession(false); if (session != null) { // Record that an authenticated user is requesting to log out try { final IPerson person = personManager.getPerson(request); if (person != null && person.getSecurityContext().isAuthenticated()) { this.portalEventFactory.publishLogoutEvent(request, this, person); } } catch (final Exception e) { log.error("Exception recording logout " + "associated with request " + request, e); } final String originalUid = this.identitySwapperManager.getOriginalUsername(session); //Logging out from a swapped user, just redirect to the Login servlet if (originalUid != null) { redirect = request.getContextPath() + "/Login"; } else { // Clear out the existing session for the user try { session.invalidate(); } catch (final IllegalStateException ise) { // IllegalStateException indicates session was already invalidated. // This is fine. LogoutController is looking to guarantee the logged out session is invalid; // it need not insist that it be the one to perform the invalidating. if (log.isTraceEnabled()) { log.trace( "LogoutController encountered IllegalStateException invalidating a presumably already-invalidated session.", ise); } } } } if (log.isTraceEnabled()) { log.trace("Redirecting to " + redirect + " to send the user back to the guest page."); } final String encodedRedirectURL = response.encodeRedirectURL(redirect); response.sendRedirect(encodedRedirectURL); } /** * The redirect is determined based upon the context that passed authentication The * LogoutController looks at each authenticated context and determines if a redirect exists for * that context in the redirectMap variable (loaded from security.properties file). The redirect * is returned for the first authenticated context that has an associated redirect string. If * such a context is not found, we use the default DEFAULT_REDIRECT that was originally setup. * * <p>NOTE: This will work or not work based upon the logic in the root context. At this time, * all known security contexts extend the ChainingSecurityContext class. If a context has the * variable stopWhenAuthenticated set to false, the user may be logged into multiple security * contexts. If this is the case, the logout process currently implemented does not accommodate * multiple logouts. As a reference implemention, the current implementation assumes only one * security context has been authenticated. Modifications to perform multiple logouts should be * considered when a concrete need arises and can be handled by this class or through a change * in the ISecurityConext API where a context knows how to perform it's own logout. * * @param request * @return String representing the redirection URL */ private String getRedirectionUrl(HttpServletRequest request) { String redirect = null; final String defaultRedirect = request.getContextPath() + "/"; IPerson person = null; if (this.redirectMap == null) { return defaultRedirect; } try { // Get the person object associated with the request person = this.personManager.getPerson(request); if (person != null) { // Retrieve the security context for the user final ISecurityContext securityContext = person.getSecurityContext(); if (securityContext.isAuthenticated()) { if (log.isDebugEnabled()) { log.debug( "LogoutController::getRedirectionUrl()" + " Looking for redirect string for the root context"); } redirect = this.redirectMap.get("root"); if (redirect != null && !redirect.equals("")) { return redirect; } } final Enumeration subCtxNames = securityContext.getSubContextNames(); while (subCtxNames.hasMoreElements()) { final String subCtxName = (String) subCtxNames.nextElement(); if (log.isDebugEnabled()) { log.debug( "LogoutController::getRedirectionUrl() " + " subCtxName = " + subCtxName); } // strip off "root." part of name final ISecurityContext sc = securityContext.getSubContext(subCtxName); if (log.isDebugEnabled()) { log.debug( "LogoutController::getRedirectionUrl()" + " subCtxName isAuth = " + sc.isAuthenticated()); } if (sc.isAuthenticated()) { if (log.isDebugEnabled()) { log.debug( "LogoutController::getRedirectionUrl()" + " Looking for redirect string for subCtxName = " + subCtxName); } redirect = this.redirectMap.get(subCtxName); if (redirect != null && !redirect.equals("")) { if (log.isDebugEnabled()) { log.debug( "LogoutController::getRedirectionUrl()" + " subCtxName redirect = " + redirect); } break; } } } } } catch (final Exception e) { // Log the exception log.error("LogoutController::getRedirectionUrl() Error:", e); } if (redirect == null) { redirect = defaultRedirect; } if (log.isDebugEnabled()) { log.debug("LogoutController::getRedirectionUrl()" + " redirectionURL = " + redirect); } return redirect; } }