001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.catalina.authenticator;
019:
020: import java.io.IOException;
021: import java.security.Principal;
022: import java.util.HashMap;
023: import java.util.Map;
024:
025: import javax.servlet.ServletException;
026: import javax.servlet.http.Cookie;
027:
028: import org.apache.catalina.Lifecycle;
029: import org.apache.catalina.LifecycleException;
030: import org.apache.catalina.LifecycleListener;
031: import org.apache.catalina.Realm;
032: import org.apache.catalina.Session;
033: import org.apache.catalina.SessionEvent;
034: import org.apache.catalina.SessionListener;
035: import org.apache.catalina.connector.Request;
036: import org.apache.catalina.connector.Response;
037: import org.apache.catalina.util.LifecycleSupport;
038: import org.apache.catalina.util.StringManager;
039: import org.apache.catalina.valves.ValveBase;
040:
041: /**
042: * A <strong>Valve</strong> that supports a "single sign on" user experience,
043: * where the security identity of a user who successfully authenticates to one
044: * web application is propogated to other web applications in the same
045: * security domain. For successful use, the following requirements must
046: * be met:
047: * <ul>
048: * <li>This Valve must be configured on the Container that represents a
049: * virtual host (typically an implementation of <code>Host</code>).</li>
050: * <li>The <code>Realm</code> that contains the shared user and role
051: * information must be configured on the same Container (or a higher
052: * one), and not overridden at the web application level.</li>
053: * <li>The web applications themselves must use one of the standard
054: * Authenticators found in the
055: * <code>org.apache.catalina.authenticator</code> package.</li>
056: * </ul>
057: *
058: * @author Craig R. McClanahan
059: * @version $Revision: 536380 $ $Date: 2007-05-09 01:49:56 +0200 (mer., 09 mai 2007) $
060: */
061:
062: public class SingleSignOn extends ValveBase implements Lifecycle,
063: SessionListener {
064:
065: // ----------------------------------------------------- Instance Variables
066:
067: /**
068: * The cache of SingleSignOnEntry instances for authenticated Principals,
069: * keyed by the cookie value that is used to select them.
070: */
071: protected Map<String, SingleSignOnEntry> cache = new HashMap<String, SingleSignOnEntry>();
072:
073: /**
074: * Descriptive information about this Valve implementation.
075: */
076: protected static String info = "org.apache.catalina.authenticator.SingleSignOn";
077:
078: /**
079: * The lifecycle event support for this component.
080: */
081: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
082:
083: /**
084: * Indicates whether this valve should require a downstream Authenticator to
085: * reauthenticate each request, or if it itself can bind a UserPrincipal
086: * and AuthType object to the request.
087: */
088: private boolean requireReauthentication = false;
089:
090: /**
091: * The cache of single sign on identifiers, keyed by the Session that is
092: * associated with them.
093: */
094: protected Map<Session, String> reverse = new HashMap<Session, String>();
095:
096: /**
097: * The string manager for this package.
098: */
099: protected final static StringManager sm = StringManager
100: .getManager(Constants.Package);
101:
102: /**
103: * Component started flag.
104: */
105: protected boolean started = false;
106:
107: /**
108: * Optional SSO cookie domain.
109: */
110: private String cookieDomain;
111:
112: // ------------------------------------------------------------- Properties
113:
114: /**
115: * Returns the optional cookie domain.
116: * May return null.
117: *
118: * @return The cookie domain
119: */
120: public String getCookieDomain() {
121: return cookieDomain;
122: }
123:
124: /**
125: * Sets the domain to be used for sso cookies.
126: *
127: * @param cookieDomain cookie domain name
128: */
129: public void setCookieDomain(String cookieDomain) {
130: if (cookieDomain != null && cookieDomain.trim().length() == 0) {
131: cookieDomain = null;
132: }
133: this .cookieDomain = cookieDomain;
134: }
135:
136: /**
137: * Gets whether each request needs to be reauthenticated (by an
138: * Authenticator downstream in the pipeline) to the security
139: * <code>Realm</code>, or if this Valve can itself bind security info
140: * to the request based on the presence of a valid SSO entry without
141: * rechecking with the <code>Realm</code..
142: *
143: * @return <code>true</code> if it is required that a downstream
144: * Authenticator reauthenticate each request before calls to
145: * <code>HttpServletRequest.setUserPrincipal()</code>
146: * and <code>HttpServletRequest.setAuthType()</code> are made;
147: * <code>false</code> if the <code>Valve</code> can itself make
148: * those calls relying on the presence of a valid SingleSignOn
149: * entry associated with the request.
150: *
151: * @see #setRequireReauthentication
152: */
153: public boolean getRequireReauthentication() {
154: return requireReauthentication;
155: }
156:
157: /**
158: * Sets whether each request needs to be reauthenticated (by an
159: * Authenticator downstream in the pipeline) to the security
160: * <code>Realm</code>, or if this Valve can itself bind security info
161: * to the request, based on the presence of a valid SSO entry, without
162: * rechecking with the <code>Realm</code.
163: * <p>
164: * If this property is <code>false</code> (the default), this
165: * <code>Valve</code> will bind a UserPrincipal and AuthType to the request
166: * if a valid SSO entry is associated with the request. It will not notify
167: * the security <code>Realm</code> of the incoming request.
168: * <p>
169: * This property should be set to <code>true</code> if the overall server
170: * configuration requires that the <code>Realm</code> reauthenticate each
171: * request thread. An example of such a configuration would be one where
172: * the <code>Realm</code> implementation provides security for both a
173: * web tier and an associated EJB tier, and needs to set security
174: * credentials on each request thread in order to support EJB access.
175: * <p>
176: * If this property is set to <code>true</code>, this Valve will set flags
177: * on the request notifying the downstream Authenticator that the request
178: * is associated with an SSO session. The Authenticator will then call its
179: * {@link AuthenticatorBase#reauthenticateFromSSO reauthenticateFromSSO}
180: * method to attempt to reauthenticate the request to the
181: * <code>Realm</code>, using any credentials that were cached with this
182: * Valve.
183: * <p>
184: * The default value of this property is <code>false</code>, in order
185: * to maintain backward compatibility with previous versions of Tomcat.
186: *
187: * @param required <code>true</code> if it is required that a downstream
188: * Authenticator reauthenticate each request before calls
189: * to <code>HttpServletRequest.setUserPrincipal()</code>
190: * and <code>HttpServletRequest.setAuthType()</code> are
191: * made; <code>false</code> if the <code>Valve</code> can
192: * itself make those calls relying on the presence of a
193: * valid SingleSignOn entry associated with the request.
194: *
195: * @see AuthenticatorBase#reauthenticateFromSSO
196: */
197: public void setRequireReauthentication(boolean required) {
198: this .requireReauthentication = required;
199: }
200:
201: // ------------------------------------------------------ Lifecycle Methods
202:
203: /**
204: * Add a lifecycle event listener to this component.
205: *
206: * @param listener The listener to add
207: */
208: public void addLifecycleListener(LifecycleListener listener) {
209:
210: lifecycle.addLifecycleListener(listener);
211:
212: }
213:
214: /**
215: * Get the lifecycle listeners associated with this lifecycle. If this
216: * Lifecycle has no listeners registered, a zero-length array is returned.
217: */
218: public LifecycleListener[] findLifecycleListeners() {
219:
220: return lifecycle.findLifecycleListeners();
221:
222: }
223:
224: /**
225: * Remove a lifecycle event listener from this component.
226: *
227: * @param listener The listener to remove
228: */
229: public void removeLifecycleListener(LifecycleListener listener) {
230:
231: lifecycle.removeLifecycleListener(listener);
232:
233: }
234:
235: /**
236: * Prepare for the beginning of active use of the public methods of this
237: * component. This method should be called after <code>configure()</code>,
238: * and before any of the public methods of the component are utilized.
239: *
240: * @exception LifecycleException if this component detects a fatal error
241: * that prevents this component from being used
242: */
243: public void start() throws LifecycleException {
244:
245: // Validate and update our current component state
246: if (started)
247: throw new LifecycleException(sm
248: .getString("authenticator.alreadyStarted"));
249: lifecycle.fireLifecycleEvent(START_EVENT, null);
250: started = true;
251:
252: }
253:
254: /**
255: * Gracefully terminate the active use of the public methods of this
256: * component. This method should be the last one called on a given
257: * instance of this component.
258: *
259: * @exception LifecycleException if this component detects a fatal error
260: * that needs to be reported
261: */
262: public void stop() throws LifecycleException {
263:
264: // Validate and update our current component state
265: if (!started)
266: throw new LifecycleException(sm
267: .getString("authenticator.notStarted"));
268: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
269: started = false;
270:
271: }
272:
273: // ------------------------------------------------ SessionListener Methods
274:
275: /**
276: * Acknowledge the occurrence of the specified event.
277: *
278: * @param event SessionEvent that has occurred
279: */
280: public void sessionEvent(SessionEvent event) {
281:
282: // We only care about session destroyed events
283: if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType())
284: && (!Session.SESSION_PASSIVATED_EVENT.equals(event
285: .getType())))
286: return;
287:
288: // Look up the single session id associated with this session (if any)
289: Session session = event.getSession();
290: if (containerLog.isDebugEnabled())
291: containerLog.debug("Process session destroyed on "
292: + session);
293:
294: String ssoId = null;
295: synchronized (reverse) {
296: ssoId = (String) reverse.get(session);
297: }
298: if (ssoId == null)
299: return;
300:
301: // Was the session destroyed as the result of a timeout?
302: // If so, we'll just remove the expired session from the
303: // SSO. If the session was logged out, we'll log out
304: // of all session associated with the SSO.
305: if (((session.getMaxInactiveInterval() > 0) && (System
306: .currentTimeMillis()
307: - session.getLastAccessedTimeInternal() >= session
308: .getMaxInactiveInterval() * 1000))
309: || (Session.SESSION_PASSIVATED_EVENT.equals(event
310: .getType()))) {
311: removeSession(ssoId, session);
312: } else {
313: // The session was logged out.
314: // Deregister this single session id, invalidating
315: // associated sessions
316: deregister(ssoId);
317: }
318:
319: }
320:
321: // ---------------------------------------------------------- Valve Methods
322:
323: /**
324: * Return descriptive information about this Valve implementation.
325: */
326: public String getInfo() {
327:
328: return (info);
329:
330: }
331:
332: /**
333: * Perform single-sign-on support processing for this request.
334: *
335: * @param request The servlet request we are processing
336: * @param response The servlet response we are creating
337: *
338: * @exception IOException if an input/output error occurs
339: * @exception ServletException if a servlet error occurs
340: */
341: public void invoke(Request request, Response response)
342: throws IOException, ServletException {
343:
344: request.removeNote(Constants.REQ_SSOID_NOTE);
345:
346: // Has a valid user already been authenticated?
347: if (containerLog.isDebugEnabled())
348: containerLog.debug("Process request for '"
349: + request.getRequestURI() + "'");
350: if (request.getUserPrincipal() != null) {
351: if (containerLog.isDebugEnabled())
352: containerLog.debug(" Principal '"
353: + request.getUserPrincipal().getName()
354: + "' has already been authenticated");
355: getNext().invoke(request, response);
356: return;
357: }
358:
359: // Check for the single sign on cookie
360: if (containerLog.isDebugEnabled())
361: containerLog.debug(" Checking for SSO cookie");
362: Cookie cookie = null;
363: Cookie cookies[] = request.getCookies();
364: if (cookies == null)
365: cookies = new Cookie[0];
366: for (int i = 0; i < cookies.length; i++) {
367: if (Constants.SINGLE_SIGN_ON_COOKIE.equals(cookies[i]
368: .getName())) {
369: cookie = cookies[i];
370: break;
371: }
372: }
373: if (cookie == null) {
374: if (containerLog.isDebugEnabled())
375: containerLog.debug(" SSO cookie is not present");
376: getNext().invoke(request, response);
377: return;
378: }
379:
380: // Look up the cached Principal associated with this cookie value
381: if (containerLog.isDebugEnabled())
382: containerLog.debug(" Checking for cached principal for "
383: + cookie.getValue());
384: SingleSignOnEntry entry = lookup(cookie.getValue());
385: if (entry != null) {
386: if (containerLog.isDebugEnabled())
387: containerLog.debug(" Found cached principal '"
388: + (entry.getPrincipal() != null ? entry
389: .getPrincipal().getName() : "")
390: + "' with auth type '" + entry.getAuthType()
391: + "'");
392: request
393: .setNote(Constants.REQ_SSOID_NOTE, cookie
394: .getValue());
395: // Only set security elements if reauthentication is not required
396: if (!getRequireReauthentication()) {
397: request.setAuthType(entry.getAuthType());
398: request.setUserPrincipal(entry.getPrincipal());
399: }
400: } else {
401: if (containerLog.isDebugEnabled())
402: containerLog
403: .debug(" No cached principal found, erasing SSO cookie");
404: cookie.setMaxAge(0);
405: response.addCookie(cookie);
406: }
407:
408: // Invoke the next Valve in our pipeline
409: getNext().invoke(request, response);
410:
411: }
412:
413: // --------------------------------------------------------- Public Methods
414:
415: /**
416: * Return a String rendering of this object.
417: */
418: public String toString() {
419:
420: StringBuffer sb = new StringBuffer("SingleSignOn[");
421: if (container == null)
422: sb.append("Container is null");
423: else
424: sb.append(container.getName());
425: sb.append("]");
426: return (sb.toString());
427:
428: }
429:
430: // ------------------------------------------------------ Protected Methods
431:
432: /**
433: * Associate the specified single sign on identifier with the
434: * specified Session.
435: *
436: * @param ssoId Single sign on identifier
437: * @param session Session to be associated
438: */
439: protected void associate(String ssoId, Session session) {
440:
441: if (containerLog.isDebugEnabled())
442: containerLog.debug("Associate sso id " + ssoId
443: + " with session " + session);
444:
445: SingleSignOnEntry sso = lookup(ssoId);
446: if (sso != null)
447: sso.addSession(this , session);
448: synchronized (reverse) {
449: reverse.put(session, ssoId);
450: }
451:
452: }
453:
454: /**
455: * Deregister the specified session. If it is the last session,
456: * then also get rid of the single sign on identifier
457: *
458: * @param ssoId Single sign on identifier
459: * @param session Session to be deregistered
460: */
461: protected void deregister(String ssoId, Session session) {
462:
463: synchronized (reverse) {
464: reverse.remove(session);
465: }
466:
467: SingleSignOnEntry sso = lookup(ssoId);
468: if (sso == null)
469: return;
470:
471: sso.removeSession(session);
472:
473: // see if we are the last session, if so blow away ssoId
474: Session sessions[] = sso.findSessions();
475: if (sessions == null || sessions.length == 0) {
476: synchronized (cache) {
477: sso = (SingleSignOnEntry) cache.remove(ssoId);
478: }
479: }
480:
481: }
482:
483: /**
484: * Deregister the specified single sign on identifier, and invalidate
485: * any associated sessions.
486: *
487: * @param ssoId Single sign on identifier to deregister
488: */
489: protected void deregister(String ssoId) {
490:
491: if (containerLog.isDebugEnabled())
492: containerLog.debug("Deregistering sso id '" + ssoId + "'");
493:
494: // Look up and remove the corresponding SingleSignOnEntry
495: SingleSignOnEntry sso = null;
496: synchronized (cache) {
497: sso = (SingleSignOnEntry) cache.remove(ssoId);
498: }
499:
500: if (sso == null)
501: return;
502:
503: // Expire any associated sessions
504: Session sessions[] = sso.findSessions();
505: for (int i = 0; i < sessions.length; i++) {
506: if (containerLog.isTraceEnabled())
507: containerLog.trace(" Invalidating session "
508: + sessions[i]);
509: // Remove from reverse cache first to avoid recursion
510: synchronized (reverse) {
511: reverse.remove(sessions[i]);
512: }
513: // Invalidate this session
514: sessions[i].expire();
515: }
516:
517: // NOTE: Clients may still possess the old single sign on cookie,
518: // but it will be removed on the next request since it is no longer
519: // in the cache
520:
521: }
522:
523: /**
524: * Attempts reauthentication to the given <code>Realm</code> using
525: * the credentials associated with the single sign-on session
526: * identified by argument <code>ssoId</code>.
527: * <p>
528: * If reauthentication is successful, the <code>Principal</code> and
529: * authorization type associated with the SSO session will be bound
530: * to the given <code>Request</code> object via calls to
531: * {@link Request#setAuthType Request.setAuthType()} and
532: * {@link Request#setUserPrincipal Request.setUserPrincipal()}
533: * </p>
534: *
535: * @param ssoId identifier of SingleSignOn session with which the
536: * caller is associated
537: * @param realm Realm implementation against which the caller is to
538: * be authenticated
539: * @param request the request that needs to be authenticated
540: *
541: * @return <code>true</code> if reauthentication was successful,
542: * <code>false</code> otherwise.
543: */
544: protected boolean reauthenticate(String ssoId, Realm realm,
545: Request request) {
546:
547: if (ssoId == null || realm == null)
548: return false;
549:
550: boolean reauthenticated = false;
551:
552: SingleSignOnEntry entry = lookup(ssoId);
553: if (entry != null && entry.getCanReauthenticate()) {
554:
555: String username = entry.getUsername();
556: if (username != null) {
557: Principal reauthPrincipal = realm.authenticate(
558: username, entry.getPassword());
559: if (reauthPrincipal != null) {
560: reauthenticated = true;
561: // Bind the authorization credentials to the request
562: request.setAuthType(entry.getAuthType());
563: request.setUserPrincipal(reauthPrincipal);
564: }
565: }
566: }
567:
568: return reauthenticated;
569: }
570:
571: /**
572: * Register the specified Principal as being associated with the specified
573: * value for the single sign on identifier.
574: *
575: * @param ssoId Single sign on identifier to register
576: * @param principal Associated user principal that is identified
577: * @param authType Authentication type used to authenticate this
578: * user principal
579: * @param username Username used to authenticate this user
580: * @param password Password used to authenticate this user
581: */
582: protected void register(String ssoId, Principal principal,
583: String authType, String username, String password) {
584:
585: if (containerLog.isDebugEnabled())
586: containerLog.debug("Registering sso id '" + ssoId
587: + "' for user '"
588: + (principal != null ? principal.getName() : "")
589: + "' with auth type '" + authType + "'");
590:
591: synchronized (cache) {
592: cache.put(ssoId, new SingleSignOnEntry(principal, authType,
593: username, password));
594: }
595:
596: }
597:
598: /**
599: * Updates any <code>SingleSignOnEntry</code> found under key
600: * <code>ssoId</code> with the given authentication data.
601: * <p>
602: * The purpose of this method is to allow an SSO entry that was
603: * established without a username/password combination (i.e. established
604: * following DIGEST or CLIENT_CERT authentication) to be updated with
605: * a username and password if one becomes available through a subsequent
606: * BASIC or FORM authentication. The SSO entry will then be usable for
607: * reauthentication.
608: * <p>
609: * <b>NOTE:</b> Only updates the SSO entry if a call to
610: * <code>SingleSignOnEntry.getCanReauthenticate()</code> returns
611: * <code>false</code>; otherwise, it is assumed that the SSO entry already
612: * has sufficient information to allow reauthentication and that no update
613: * is needed.
614: *
615: * @param ssoId identifier of Single sign to be updated
616: * @param principal the <code>Principal</code> returned by the latest
617: * call to <code>Realm.authenticate</code>.
618: * @param authType the type of authenticator used (BASIC, CLIENT_CERT,
619: * DIGEST or FORM)
620: * @param username the username (if any) used for the authentication
621: * @param password the password (if any) used for the authentication
622: */
623: protected void update(String ssoId, Principal principal,
624: String authType, String username, String password) {
625:
626: SingleSignOnEntry sso = lookup(ssoId);
627: if (sso != null && !sso.getCanReauthenticate()) {
628: if (containerLog.isDebugEnabled())
629: containerLog.debug("Update sso id " + ssoId
630: + " to auth type " + authType);
631:
632: synchronized (sso) {
633: sso.updateCredentials(principal, authType, username,
634: password);
635: }
636:
637: }
638: }
639:
640: /**
641: * Look up and return the cached SingleSignOn entry associated with this
642: * sso id value, if there is one; otherwise return <code>null</code>.
643: *
644: * @param ssoId Single sign on identifier to look up
645: */
646: protected SingleSignOnEntry lookup(String ssoId) {
647:
648: synchronized (cache) {
649: return ((SingleSignOnEntry) cache.get(ssoId));
650: }
651:
652: }
653:
654: /**
655: * Remove a single Session from a SingleSignOn. Called when
656: * a session is timed out and no longer active.
657: *
658: * @param ssoId Single sign on identifier from which to remove the session.
659: * @param session the session to be removed.
660: */
661: protected void removeSession(String ssoId, Session session) {
662:
663: if (containerLog.isDebugEnabled())
664: containerLog.debug("Removing session " + session.toString()
665: + " from sso id " + ssoId);
666:
667: // Get a reference to the SingleSignOn
668: SingleSignOnEntry entry = lookup(ssoId);
669: if (entry == null)
670: return;
671:
672: // Remove the inactive session from SingleSignOnEntry
673: entry.removeSession(session);
674:
675: // Remove the inactive session from the 'reverse' Map.
676: synchronized (reverse) {
677: reverse.remove(session);
678: }
679:
680: // If there are not sessions left in the SingleSignOnEntry,
681: // deregister the entry.
682: if (entry.findSessions().length == 0) {
683: deregister(ssoId);
684: }
685: }
686:
687: }
|