/* * JOSSO: Java Open Single Sign-On * * Copyright 2004-2009, Atricore, Inc. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * */ package org.josso.agent; import org.josso.gateway.GatewayServiceLocator; import org.josso.gateway.assertion.exceptions.AssertionNotValidException; import org.josso.gateway.identity.service.SSOIdentityManagerService; import org.josso.gateway.identity.service.SSOIdentityProvider; import org.josso.gateway.identity.service.SSOIdentityProviderService; import org.josso.gateway.session.exceptions.FatalSSOSessionException; import org.josso.gateway.session.exceptions.NoSuchSessionException; import org.josso.gateway.session.exceptions.SSOSessionException; import org.josso.gateway.session.service.SSOSessionManager; import org.josso.gateway.session.service.SSOSessionManagerService; import java.io.IOException; import java.security.Principal; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Represents a partial implementation of an Single Sign-on Agent. * An Agent stands in between the Gateway and the Security Domain were partner application reside. * It provides transparent security context to partner applications, providing user and role information * by invoking the gateway through JAAS. * * @author <a href="mailto:gbrigand@josso.org">Gianluca Brigandi</a> * @version CVS $Id: AbstractSSOAgent.java 609 2008-08-21 19:24:02Z sgonzalez $ */ public abstract class AbstractSSOAgent implements SSOAgent { /** * The name of the cookie that holds the JOSSO Session id. */ // private static final String JOSSO_SINGLE_SIGN_ON_COOKIE = "JOSSO_SESSIONID"; public static final long DEFAULT_SESSION_ACCESS_MIN_INTERVAL = 1000; public static final ThreadLocal<SSOAgentRequest> _currentRequest = new ThreadLocal<SSOAgentRequest>(); // ----------------------------------------------------- Instance Variables /** * The cache of SingleSignOnEntry instances for authenticated Principals, * keyed by the cookie value that is used to select them. */ protected final Map<String, SingleSignOnEntry> cache = Collections.synchronizedMap(new HashMap<String, SingleSignOnEntry>()); /** * The cache of single sign on identifiers, keyed by the Session that is * associated with them. */ protected final Map<LocalSession, String> reverse = Collections.synchronizedMap(new HashMap<LocalSession, String>()); protected boolean started = false; protected int debug = 0; // ---------------<Default GWY Service Locator> protected GatewayServiceLocator gsl; protected SSOSessionManagerService smDefault; protected SSOIdentityManagerService imDefault; protected SSOIdentityProviderService ipDefault; // ---------------<Specific service locators (by node id)> protected Map<String, GatewayServiceLocator> gslsByNode = new HashMap<String, GatewayServiceLocator>(); protected Map<String, NodeServices> servicesByNode = new HashMap<String, NodeServices>(); // ---------------<Configuration properties > protected SSOAgentConfiguration _cfg; private String _gatewayLoginUrl; private String _gatewayLogoutUrl; private String _gatewayLoginErrorUrl; private String _singlePointOfAccess; private long _sessionAccessMinInterval = DEFAULT_SESSION_ACCESS_MIN_INTERVAL; private boolean _isStateOnClient = false; // --------- <Some statistical information, exposed through MBeans > private long _requestCount; private long _l1CacheHits; private long _l2CacheHits; // ----------------------------------------------------- Properties /** * Configures the Gateway Service Locator to be used by the SSOAgent. */ public void setGatewayServiceLocator(GatewayServiceLocator gsl) { this.gsl = gsl; } /** * * Obtains the Gateway Service Locator used by the SSOAgent to build * the concrete clients needed to query the Gateway services. * <p/> * This getter is need by the JAASLoginModule to know which Gateway Service Locator to use. * * @return the configured gateway service locator */ public GatewayServiceLocator getGatewayServiceLocator() { return gsl; } public SSOSessionManagerService getSSOSessionManager() { return getSSOSessionManager(null); } public SSOSessionManagerService getSSOSessionManager(String nodeId) { if (nodeId != null) { NodeServices svcs = servicesByNode.get(nodeId); if (svcs != null && svcs.getSm() != null) return svcs.getSm(); } return smDefault; } public SSOIdentityManagerService getSSOIdentityManager() { return getSSOIdentityManager(null); } public SSOIdentityManagerService getSSOIdentityManager(String nodeId) { if (nodeId != null ) { NodeServices svcs = servicesByNode.get(nodeId); if (svcs != null && svcs.getIm() != null) return svcs.getIm(); } return imDefault; } public SSOIdentityProviderService getSSOIdentityProvider() { return getSSOIdentityProvider(null); } public SSOIdentityProviderService getSSOIdentityProvider(String nodeId) { if (nodeId != null) { NodeServices svcs = servicesByNode.get(nodeId); if (svcs != null && svcs.getIp() != null) return svcs.getIp(); } return ipDefault; } public Map<String, GatewayServiceLocator> getGatewayServiceLocators() { return this.gslsByNode; } public void setGatewayServiceLocators(Map<String, GatewayServiceLocator> gslsByNode) { this.gslsByNode = gslsByNode; } /** * Sets the Login Form Url of the Gateway. */ public void setGatewayLoginUrl(String gatewayLoginUrl) { _gatewayLoginUrl = gatewayLoginUrl; } /** * Returns the Login Form Url of the Gateway. * * @return the gateway login url */ public String getGatewayLoginUrl() { return _gatewayLoginUrl; } /** * Returns the Error Login Url of the Gateway. * * @return the gateway login url * @deprecated No longer supported! */ public String getGatewayLoginErrorUrl() { return _gatewayLoginErrorUrl; } /** * Sets the Error Login Url of the Gateway. * @deprecated no longe used */ public void setGatewayLoginErrorUrl(String gatewayLoginErrorUrl) { log("gatewayLoginErrorUrl is no longer supported, modify your agent config. Check customLoginUrl in JOSSO Gwy config for alternatives."); _gatewayLoginErrorUrl = gatewayLoginErrorUrl; } /** * Sets the Logout Form Url of the Gateway. */ public void setGatewayLogoutUrl(String gatewayLogoutUrl) { _gatewayLogoutUrl = gatewayLogoutUrl; } /** * Returns the Logout Form Url of the Gateway. * * @return the gateway login url */ public String getGatewayLogoutUrl() { return _gatewayLogoutUrl; } /** * Used by the configuraiton, to set the session access min interval. */ public void setSessionAccessMinInterval(String v) { setSessionAccessMinInterval(Long.parseLong(v)); } /** * Gets the session access min interval. */ public long getSessionAccessMinInterval() { return _sessionAccessMinInterval; } /** * Sets the session access min interval. */ public void setSessionAccessMinInterval(long sessionAccessMinInterval) { _sessionAccessMinInterval = sessionAccessMinInterval; } /** * Single Point of Access to the SSO infrastructure. Useful when working in N-Tier mode behind a reverse proxy or * load balancer */ public String getSinglePointOfAccess() { return _singlePointOfAccess; } /** * Single Point of Access to the SSO infrastructure. Useful when working in N-Tier mode behind a reverse proxy or * load balancer * * @param singlePointOfAccess used in combination with reverse proxy setups. */ public void setSinglePointOfAccess(String singlePointOfAccess) { _singlePointOfAccess = singlePointOfAccess; } /** * Returns true if the received context should be processed by this agent. It means that * the context belongs to a partner application. * * @param contextPath the web application context to be checked. * @return true if this context belongs to a josso partner app. */ public boolean isPartnerApp(String vhost, String contextPath) { return getPartnerAppConfig(vhost, contextPath) != null; } /** * Returns the partner application configuration definition associtated with the given context. * If no partner application is defined for the context, returns null. */ public SSOPartnerAppConfig getPartnerAppConfig(String vhost, String contextPath) { List<SSOPartnerAppConfig> papps = _cfg.getSsoPartnerApps(); if (contextPath == null || "".equals(contextPath)) contextPath = "/"; for (SSOPartnerAppConfig ssoPartnerAppConfig : papps) { if ((ssoPartnerAppConfig.getVhost() == null || ssoPartnerAppConfig.getVhost().equals(vhost)) && contextPath.equals(ssoPartnerAppConfig.getContext())) return ssoPartnerAppConfig; } log("No partner application configured for '"+vhost+"' and '"+contextPath+"'"); return null; } /** * Starts the Agent. */ public void start() { try { smDefault = gsl.getSSOSessionManager(); imDefault = gsl.getSSOIdentityManager(); ipDefault = gsl.getSSOIdentityProvider(); for (String nodeId : gslsByNode.keySet()) { GatewayServiceLocator gsl = gslsByNode.get(nodeId); NodeServices svcs = new NodeServices(nodeId, gsl); svcs.start(); this.servicesByNode.put(nodeId, svcs); } for (SSOPartnerAppConfig cfg : _cfg.getSsoPartnerApps()) { if (cfg.getId() == null) { log("ERROR! You should define an ID for partner application " + cfg.getContext()); } cfg.getIdentityProviderService(); cfg.getIdentityManagerService(); cfg.getSessionManagerService(); } if (debug > 0) log("Agent Started"); } catch (Exception e) { log("Can't create session/identity/provider managers : \n" + e.getMessage(), e); } } /** * Authenticated a user session previously authenticated by the gateway. * * @param request the JOSSO Agent request * @return the logged user identity. */ public final SingleSignOnEntry processRequest(SSOAgentRequest request) { // We need to make the request available to other componets that responde to container contracts try { _currentRequest.set(request); return execute(request); } finally { _currentRequest.remove(); } } protected SingleSignOnEntry execute(SSOAgentRequest request) { try { // Count this request. _requestCount++; int action = request.getAction(); String jossoSessionId = request.getSessionId(); LocalSession localSession = request.getLocalSession(); if (action == SSOAgentRequest.ACTION_ASSERT_SESSION) { try { accessSession(request.getConfig(this), request.getRequester(), jossoSessionId, request.getNodeId()); } catch (SSOSessionException e) { throw new FatalSSOSessionException("Assertion error for session : " + jossoSessionId, e); } return null; } if (action == SSOAgentRequest.ACTION_CUSTOM_AUTHENTICATION){ sendCustomAuthentication(request); return null; } if (action == SSOAgentRequest.ACTION_RELAY) { String assertionId = request.getAssertionId(); jossoSessionId = resolveAssertion(request.getConfig(this), request.getRequester(), assertionId, request.getNodeId()); request.setSessionId(jossoSessionId); } // Look up the cached Principal associated with this cookie value if (debug > 0) log("Checking for cached principal for " + jossoSessionId); SingleSignOnEntry entry = lookup(jossoSessionId); if (entry != null) { if (debug > 0) log(" Found cached principal '" + entry.principal.getName() + "' with auth type '" + entry.authType + "'"); // Count the cache hit. _l1CacheHits++; entry = accessSession(request.getConfig(this), request.getRequester(), entry, jossoSessionId, request.getNodeId()); if (entry != null) { if (isAuthenticationAlwaysRequired()) { Principal p = authenticate(request); if (debug > 0) log("Updating Principal information"); entry.updatePrincipal(p); } // Even if a cached principal is present, the container-private // security context might not be, therefore force an authentication // so that the security context is recreated. // propagateSecurityContext(request, entry.principal); } return entry; } // Make the agent receive local session events. localSession.addSessionListener(this); // Associated local session to the SSO Session associateLocalSession(jossoSessionId, localSession); // Invoke the JAAS Gateway Login Module to obtain user information Principal ssoUserPrincipal = authenticate(request); if (ssoUserPrincipal != null) { if (debug > 0) log("Principal checked for SSO Session '" + jossoSessionId + "' : " + ssoUserPrincipal); register(jossoSessionId, ssoUserPrincipal, "JOSSO"); entry = lookup(jossoSessionId); entry = accessSession(request.getConfig(this), request.getRequester(), entry, jossoSessionId, request.getNodeId()); if (entry != null) propagateSecurityContext(request, entry.principal); return entry; } if (debug > 0) log("There is no associated principal for SSO Session '" + jossoSessionId + "'"); return null; } catch (Exception e) { log("Error processing JOSSO Agent request : " + e.getMessage()); if (debug > 0) log("Exception recieved while processing JOSSO Agent request : " + e.getMessage(), e); return null; } } protected void propagateSecurityContext(SSOAgentRequest request, Principal principal) { throw new java.lang.UnsupportedOperationException( "No support for alternative mechanisms for security context propagation" ); } /** * Dereference assertion id by invoking the corresponding operation using the back-channel * * @param assertionId * @return null if the authentication assertion is invalid */ protected String resolveAssertion(SSOPartnerAppConfig appConfig, String requester, String assertionId, String nodeId) { try { if (debug > 0) log("Dereferencing assertion for id '" + assertionId + "'"); SSOIdentityProviderService ip = appConfig.getIdentityProviderService(); if (ip == null) ip = getSSOIdentityProvider(nodeId); String ssoSessionId = null; if (nodeId != null && !"".equals(nodeId)) { NodeServices svcs = servicesByNode.get(nodeId); if (svcs != null) { ssoSessionId = svcs.getIp().resolveAuthenticationAssertion(requester, assertionId); } else { ssoSessionId = ip.resolveAuthenticationAssertion(requester, assertionId); } } else { ssoSessionId = ip.resolveAuthenticationAssertion(requester, assertionId); } if (debug > 0) log("Dereferencing assertion for id '" + assertionId + "' as SSO Session '"+ssoSessionId+"'"); return ssoSessionId; } catch (AssertionNotValidException e) { if (debug > 0) log("Invalid Assertion"); return null; } catch (Exception e) { log(e.getMessage() != null ? e.getMessage() : e.toString(), e); return null; } } /** * Access sso session related with given entry. If sso session is no longer valid, * deregisters the session and return null. * * @param entry * @return null if the sso session is no longer valid. */ protected SingleSignOnEntry accessSession(SSOPartnerAppConfig appConfig, String requester, SingleSignOnEntry entry, String jossoSessionId, String nodeId) { // Just in case if (entry == null) return entry; // Do not access server more than once in a second ... long now = System.currentTimeMillis(); if ((now - entry.lastAccessTime) < getSessionAccessMinInterval()) { _l2CacheHits++; return entry; } try { // send a keep-alive event for the SSO session if (debug > 0) log("Notifying keep-alive event for session '" + jossoSessionId + "'"); SSOSessionManagerService sm = appConfig.getSessionManagerService(); if (sm == null) sm = getSSOSessionManager(nodeId); if (nodeId != null && !"".equals(nodeId)) { NodeServices svcs = servicesByNode.get(nodeId); if (svcs != null) { if (debug > 0) log("Using services for node : " + nodeId); svcs.getSm().accessSession(requester, jossoSessionId); } else { if (debug > 0) log("Using default services for node : " + nodeId); sm.accessSession(requester, jossoSessionId); } } else { if (debug > 0) log("Using default services, no node found"); sm.accessSession(requester, jossoSessionId); } entry.lastAccessTime = now; return entry; } catch (NoSuchSessionException e) { if (debug > 0) log("SSO Session is no longer valid"); deregister(entry.ssoId); return null; } catch (Exception e) { log(e.getMessage() != null ? e.getMessage() : e.toString(), e); deregister(entry.ssoId); return null; } } /** * Access sso session related with given the given SSO session identifier. * In case the session is invalid or cannot be asserted an SSOException is thrown. */ protected void accessSession(SSOPartnerAppConfig appConfig, String requester, String jossoSessionId, String nodeId) throws SSOSessionException { try { // send a keep-alive event for the SSO session if (debug > 0) log("Notifying keep-alive event for session '" + jossoSessionId + "'"); SSOSessionManagerService sm = appConfig.getSessionManagerService(); if (sm == null) sm = getSSOSessionManager(nodeId); sm.accessSession(requester, jossoSessionId); } catch (NoSuchSessionException e) { if (debug > 0) log("SSO Session is no longer valid"); throw e; } catch (Exception e) { log(e.getMessage() != null ? e.getMessage() : e.toString(), e); throw new SSOSessionException(e.getMessage() != null ? e.getMessage() : e.toString(), e); } } abstract protected void sendCustomAuthentication(SSOAgentRequest request) throws IOException; /** * Template method used by the agent to obtain a principal from a SSO Agent request. * * @param request * @return the authenticated principal. */ abstract protected Principal authenticate(SSOAgentRequest request); /** * This indicates that sso agent request must be always authenticated. */ abstract protected boolean isAuthenticationAlwaysRequired(); /** * Log a message on the Logger associated with our Container (if any). * * @param message Message to be logged */ abstract protected void log(String message); /** * Log a message on the Logger associated with our Container (if any). * * @param message Message to be logged * @param throwable Associated exception */ abstract protected void log(String message, Throwable throwable); /** * Stop the Agent. */ public void stop() { if (debug > 0) log("Agent Stopped"); } // ------------------------------------------------ LocalSessionListener Methods /** * Acknowledge the occurrence of the specified event. * * @param event SessionEvent that has occurred */ public void localSessionEvent(LocalSessionEvent event) { // We only care about session destroyed events if (!LocalSession.LOCAL_SESSION_DESTROYED_EVENT.equals(event.getType())) return; // Look up the single session id associated with this session (if any) LocalSession session = event.getLocalSession(); if (debug > 0) log("Local session destroyed on " + session); // notify gateway that the session was destroyed. localSessionDestroyedEvent(session); } public void setConfiguration(SSOAgentConfiguration cfg) { _cfg = cfg; } public SSOAgentConfiguration getConfiguration() { return _cfg; } /** * Disassociates a Local Session from the Single Sign-on session since the Local Session * was destroyed. * * @param session */ protected void localSessionDestroyedEvent(LocalSession session) { String ssoId = null; synchronized (reverse) { ssoId = reverse.remove(session); } if (ssoId == null) return; // Deregister this single sso session id, invalidating associated sessions deregister(ssoId); } /** * Associate the specified single sign on identifier with the * specified Session. * * @param ssoId Single sign on identifier * @param localSession Local Session to be associated to the SSO Session. */ protected void associateLocalSession(String ssoId, LocalSession localSession) { SingleSignOnEntry sso = lookup(ssoId); if (sso != null) sso.addSession(localSession); synchronized (reverse) { reverse.put(localSession, ssoId); } } /** * Deregister the specified single sign on identifier, and invalidate * any associated sessions. * * @param ssoId Single sign on identifier to deregister */ protected void deregister(String ssoId) { // Look up and remove the corresponding SingleSignOnEntry SingleSignOnEntry sso = null; synchronized (cache) { sso = (SingleSignOnEntry) cache.remove(ssoId); } } /** * Register the specified Principal as being associated with the specified * value for the single sign on identifier. * * @param ssoId Single sign on identifier to register * @param principal Associated user principal that is identified * @param authType Authentication type used to authenticate this * user principal */ protected void register(String ssoId, Principal principal, String authType) { synchronized (cache) { cache.put(ssoId, new SingleSignOnEntry(ssoId, principal, authType)); } } /** * Look up and return the cached SingleSignOn entry associated with this * sso id value, if there is one; otherwise return <code>null</code>. * * @param ssoId Single sign on identifier to look up */ protected SingleSignOnEntry lookup(String ssoId) { synchronized (cache) { return cache.get(ssoId); } } public int getDebug() { return debug; } public void setDebug(int debug) { this.debug = debug; } public long getRequestCount() { return _requestCount; } public long getL1CacheHits() { return _l1CacheHits; } public long getL2CacheHits() { return _l2CacheHits; } public boolean isStateOnClient() { return _isStateOnClient; } public void setIsStateOnClient(boolean isStateOnClient) { _isStateOnClient = isStateOnClient; } public void setStateOnClient(boolean isStateOnClient) { _isStateOnClient = isStateOnClient; } public class NodeServices { private String nodeId; protected GatewayServiceLocator gsl; protected SSOSessionManagerService sm; protected SSOIdentityManagerService im; protected SSOIdentityProviderService ip; public NodeServices(String nodeId, GatewayServiceLocator gsl) { this.nodeId = nodeId; this.gsl = gsl; } public void start() { try { sm = gsl.getSSOSessionManager(); im = gsl.getSSOIdentityManager(); ip = gsl.getSSOIdentityProvider(); if (debug > 0) log("Agent Services clients started for " + nodeId); } catch (Exception e) { log("Can't create session/identity/provider managers : \n" + e.getMessage(), e); } } public SSOSessionManagerService getSm() { return sm; } public SSOIdentityManagerService getIm() { return im; } public SSOIdentityProviderService getIp() { return ip; } } }