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