001: /*
002: * $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/authenticator/SingleSignOn.java,v 1.11 2002/06/09 02:19:41 remm Exp $
003: * $Revision: 1.11 $
004: * $Date: 2002/06/09 02:19:41 $
005: *
006: * ====================================================================
007: *
008: * The Apache Software License, Version 1.1
009: *
010: * Copyright (c) 1999-2001 The Apache Software Foundation. All rights
011: * reserved.
012: *
013: * Redistribution and use in source and binary forms, with or without
014: * modification, are permitted provided that the following conditions
015: * are met:
016: *
017: * 1. Redistributions of source code must retain the above copyright
018: * notice, this list of conditions and the following disclaimer.
019: *
020: * 2. Redistributions in binary form must reproduce the above copyright
021: * notice, this list of conditions and the following disclaimer in
022: * the documentation and/or other materials provided with the
023: * distribution.
024: *
025: * 3. The end-user documentation included with the redistribution, if
026: * any, must include the following acknowlegement:
027: * "This product includes software developed by the
028: * Apache Software Foundation (http://www.apache.org/)."
029: * Alternately, this acknowlegement may appear in the software itself,
030: * if and wherever such third-party acknowlegements normally appear.
031: *
032: * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
033: * Foundation" must not be used to endorse or promote products derived
034: * from this software without prior written permission. For written
035: * permission, please contact apache@apache.org.
036: *
037: * 5. Products derived from this software may not be called "Apache"
038: * nor may "Apache" appear in their names without prior written
039: * permission of the Apache Group.
040: *
041: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
042: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
043: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
044: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
045: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
046: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
047: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
048: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
049: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
050: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
051: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
052: * SUCH DAMAGE.
053: * ====================================================================
054: *
055: * This software consists of voluntary contributions made by many
056: * individuals on behalf of the Apache Software Foundation. For more
057: * information on the Apache Software Foundation, please see
058: * <http://www.apache.org/>.
059: *
060: * [Additional notices, if required by prior licensing conditions]
061: *
062: */
063:
064: package org.apache.catalina.authenticator;
065:
066: import java.io.IOException;
067: import java.security.Principal;
068: import java.util.HashMap;
069: import javax.servlet.ServletException;
070: import javax.servlet.http.Cookie;
071: import javax.servlet.http.HttpServletRequest;
072: import javax.servlet.http.HttpServletResponse;
073: import org.apache.catalina.Container;
074: import org.apache.catalina.HttpRequest;
075: import org.apache.catalina.HttpResponse;
076: import org.apache.catalina.Lifecycle;
077: import org.apache.catalina.LifecycleEvent;
078: import org.apache.catalina.LifecycleException;
079: import org.apache.catalina.LifecycleListener;
080: import org.apache.catalina.Logger;
081: import org.apache.catalina.Request;
082: import org.apache.catalina.Response;
083: import org.apache.catalina.Session;
084: import org.apache.catalina.SessionEvent;
085: import org.apache.catalina.SessionListener;
086: import org.apache.catalina.ValveContext;
087: import org.apache.catalina.valves.ValveBase;
088: import org.apache.catalina.util.LifecycleSupport;
089: import org.apache.catalina.util.StringManager;
090:
091: /**
092: * A <strong>Valve</strong> that supports a "single sign on" user experience,
093: * where the security identity of a user who successfully authenticates to one
094: * web application is propogated to other web applications in the same
095: * security domain. For successful use, the following requirements must
096: * be met:
097: * <ul>
098: * <li>This Valve must be configured on the Container that represents a
099: * virtual host (typically an implementation of <code>Host</code>).</li>
100: * <li>The <code>Realm</code> that contains the shared user and role
101: * information must be configured on the same Container (or a higher
102: * one), and not overridden at the web application level.</li>
103: * <li>The web applications themselves must use one of the standard
104: * Authenticators found in the
105: * <code>org.apache.catalina.authenticator</code> package.</li>
106: * </ul>
107: *
108: * @author Craig R. McClanahan
109: * @version $Revision: 1.11 $ $Date: 2002/06/09 02:19:41 $
110: */
111:
112: public class SingleSignOn extends ValveBase implements Lifecycle,
113: SessionListener {
114:
115: // ----------------------------------------------------- Instance Variables
116:
117: /**
118: * The cache of SingleSignOnEntry instances for authenticated Principals,
119: * keyed by the cookie value that is used to select them.
120: */
121: protected HashMap cache = new HashMap();
122:
123: /**
124: * The debugging detail level for this component.
125: */
126: protected int debug = 0;
127:
128: /**
129: * Descriptive information about this Valve implementation.
130: */
131: protected static String info = "org.apache.catalina.authenticator.SingleSignOn";
132:
133: /**
134: * The lifecycle event support for this component.
135: */
136: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
137:
138: /**
139: * The cache of single sign on identifiers, keyed by the Session that is
140: * associated with them.
141: */
142: protected HashMap reverse = new HashMap();
143:
144: /**
145: * The string manager for this package.
146: */
147: protected final static StringManager sm = StringManager
148: .getManager(Constants.Package);
149:
150: /**
151: * Component started flag.
152: */
153: protected boolean started = false;
154:
155: // ------------------------------------------------------------- Properties
156:
157: /**
158: * Return the debugging detail level.
159: */
160: public int getDebug() {
161:
162: return (this .debug);
163:
164: }
165:
166: /**
167: * Set the debugging detail level.
168: *
169: * @param debug The new debugging detail level
170: */
171: public void setDebug(int debug) {
172:
173: this .debug = debug;
174:
175: }
176:
177: // ------------------------------------------------------ Lifecycle Methods
178:
179: /**
180: * Add a lifecycle event listener to this component.
181: *
182: * @param listener The listener to add
183: */
184: public void addLifecycleListener(LifecycleListener listener) {
185:
186: lifecycle.addLifecycleListener(listener);
187:
188: }
189:
190: /**
191: * Get the lifecycle listeners associated with this lifecycle. If this
192: * Lifecycle has no listeners registered, a zero-length array is returned.
193: */
194: public LifecycleListener[] findLifecycleListeners() {
195:
196: return lifecycle.findLifecycleListeners();
197:
198: }
199:
200: /**
201: * Remove a lifecycle event listener from this component.
202: *
203: * @param listener The listener to remove
204: */
205: public void removeLifecycleListener(LifecycleListener listener) {
206:
207: lifecycle.removeLifecycleListener(listener);
208:
209: }
210:
211: /**
212: * Prepare for the beginning of active use of the public methods of this
213: * component. This method should be called after <code>configure()</code>,
214: * and before any of the public methods of the component are utilized.
215: *
216: * @exception LifecycleException if this component detects a fatal error
217: * that prevents this component from being used
218: */
219: public void start() throws LifecycleException {
220:
221: // Validate and update our current component state
222: if (started)
223: throw new LifecycleException(sm
224: .getString("authenticator.alreadyStarted"));
225: lifecycle.fireLifecycleEvent(START_EVENT, null);
226: started = true;
227:
228: if (debug >= 1)
229: log("Started");
230:
231: }
232:
233: /**
234: * Gracefully terminate the active use of the public methods of this
235: * component. This method should be the last one called on a given
236: * instance of this component.
237: *
238: * @exception LifecycleException if this component detects a fatal error
239: * that needs to be reported
240: */
241: public void stop() throws LifecycleException {
242:
243: // Validate and update our current component state
244: if (!started)
245: throw new LifecycleException(sm
246: .getString("authenticator.notStarted"));
247: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
248: started = false;
249:
250: if (debug >= 1)
251: log("Stopped");
252:
253: }
254:
255: // ------------------------------------------------ SessionListener Methods
256:
257: /**
258: * Acknowledge the occurrence of the specified event.
259: *
260: * @param event SessionEvent that has occurred
261: */
262: public void sessionEvent(SessionEvent event) {
263:
264: // We only care about session destroyed events
265: if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType()))
266: return;
267:
268: // Look up the single session id associated with this session (if any)
269: Session session = event.getSession();
270: if (debug >= 1)
271: log("Process session destroyed on " + session);
272: String ssoId = null;
273: synchronized (reverse) {
274: ssoId = (String) reverse.get(session);
275: }
276: if (ssoId == null)
277: return;
278:
279: // Deregister this single session id, invalidating associated sessions
280: deregister(ssoId);
281:
282: }
283:
284: // ---------------------------------------------------------- Valve Methods
285:
286: /**
287: * Return descriptive information about this Valve implementation.
288: */
289: public String getInfo() {
290:
291: return (info);
292:
293: }
294:
295: /**
296: * Perform single-sign-on support processing for this request.
297: *
298: * @param request The servlet request we are processing
299: * @param response The servlet response we are creating
300: * @param context The valve context used to invoke the next valve
301: * in the current processing pipeline
302: *
303: * @exception IOException if an input/output error occurs
304: * @exception ServletException if a servlet error occurs
305: */
306: public void invoke(Request request, Response response,
307: ValveContext context) throws IOException, ServletException {
308:
309: // If this is not an HTTP request and response, just pass them on
310: if (!(request instanceof HttpRequest)
311: || !(response instanceof HttpResponse)) {
312: context.invokeNext(request, response);
313: return;
314: }
315: HttpServletRequest hreq = (HttpServletRequest) request
316: .getRequest();
317: HttpServletResponse hres = (HttpServletResponse) response
318: .getResponse();
319: request.removeNote(Constants.REQ_SSOID_NOTE);
320:
321: // Has a valid user already been authenticated?
322: if (debug >= 1)
323: log("Process request for '" + hreq.getRequestURI() + "'");
324: if (hreq.getUserPrincipal() != null) {
325: if (debug >= 1)
326: log(" Principal '" + hreq.getUserPrincipal().getName()
327: + "' has already been authenticated");
328: context.invokeNext(request, response);
329: return;
330: }
331:
332: // Check for the single sign on cookie
333: if (debug >= 1)
334: log(" Checking for SSO cookie");
335: Cookie cookie = null;
336: Cookie cookies[] = hreq.getCookies();
337: if (cookies == null)
338: cookies = new Cookie[0];
339: for (int i = 0; i < cookies.length; i++) {
340: if (Constants.SINGLE_SIGN_ON_COOKIE.equals(cookies[i]
341: .getName())) {
342: cookie = cookies[i];
343: break;
344: }
345: }
346: if (cookie == null) {
347: if (debug >= 1)
348: log(" SSO cookie is not present");
349: context.invokeNext(request, response);
350: return;
351: }
352:
353: // Look up the cached Principal associated with this cookie value
354: if (debug >= 1)
355: log(" Checking for cached principal for "
356: + cookie.getValue());
357: SingleSignOnEntry entry = lookup(cookie.getValue());
358: if (entry != null) {
359: if (debug >= 1)
360: log(" Found cached principal '"
361: + entry.principal.getName()
362: + "' with auth type '" + entry.authType + "'");
363: request
364: .setNote(Constants.REQ_SSOID_NOTE, cookie
365: .getValue());
366: ((HttpRequest) request).setAuthType(entry.authType);
367: ((HttpRequest) request).setUserPrincipal(entry.principal);
368: } else {
369: if (debug >= 1)
370: log(" No cached principal found, erasing SSO cookie");
371: cookie.setMaxAge(0);
372: hres.addCookie(cookie);
373: }
374:
375: // Invoke the next Valve in our pipeline
376: context.invokeNext(request, response);
377:
378: }
379:
380: // --------------------------------------------------------- Public Methods
381:
382: /**
383: * Return a String rendering of this object.
384: */
385: public String toString() {
386:
387: StringBuffer sb = new StringBuffer("SingleSignOn[");
388: sb.append(container.getName());
389: sb.append("]");
390: return (sb.toString());
391:
392: }
393:
394: // -------------------------------------------------------- Package Methods
395:
396: /**
397: * Associate the specified single sign on identifier with the
398: * specified Session.
399: *
400: * @param ssoId Single sign on identifier
401: * @param session Session to be associated
402: */
403: void associate(String ssoId, Session session) {
404:
405: if (debug >= 1)
406: log("Associate sso id " + ssoId + " with session "
407: + session);
408:
409: SingleSignOnEntry sso = lookup(ssoId);
410: if (sso != null)
411: sso.addSession(this , session);
412: synchronized (reverse) {
413: reverse.put(session, ssoId);
414: }
415:
416: }
417:
418: /**
419: * Deregister the specified single sign on identifier, and invalidate
420: * any associated sessions.
421: *
422: * @param ssoId Single sign on identifier to deregister
423: */
424: void deregister(String ssoId) {
425:
426: if (debug >= 1)
427: log("Deregistering sso id '" + ssoId + "'");
428:
429: // Look up and remove the corresponding SingleSignOnEntry
430: SingleSignOnEntry sso = null;
431: synchronized (cache) {
432: sso = (SingleSignOnEntry) cache.remove(ssoId);
433: }
434: if (sso == null)
435: return;
436:
437: // Expire any associated sessions
438: Session sessions[] = sso.findSessions();
439: for (int i = 0; i < sessions.length; i++) {
440: if (debug >= 2)
441: log(" Invalidating session " + sessions[i]);
442: // Remove from reverse cache first to avoid recursion
443: synchronized (reverse) {
444: reverse.remove(sessions[i]);
445: }
446: // Invalidate this session
447: sessions[i].expire();
448: }
449:
450: // NOTE: Clients may still possess the old single sign on cookie,
451: // but it will be removed on the next request since it is no longer
452: // in the cache
453:
454: }
455:
456: /**
457: * Register the specified Principal as being associated with the specified
458: * value for the single sign on identifier.
459: *
460: * @param ssoId Single sign on identifier to register
461: * @param principal Associated user principal that is identified
462: * @param authType Authentication type used to authenticate this
463: * user principal
464: * @param username Username used to authenticate this user
465: * @param password Password used to authenticate this user
466: */
467: void register(String ssoId, Principal principal, String authType,
468: String username, String password) {
469:
470: if (debug >= 1)
471: log("Registering sso id '" + ssoId + "' for user '"
472: + principal.getName() + "' with auth type '"
473: + authType + "'");
474:
475: synchronized (cache) {
476: cache.put(ssoId, new SingleSignOnEntry(principal, authType,
477: username, password));
478: }
479:
480: }
481:
482: // ------------------------------------------------------ Protected Methods
483:
484: /**
485: * Log a message on the Logger associated with our Container (if any).
486: *
487: * @param message Message to be logged
488: */
489: protected void log(String message) {
490:
491: Logger logger = container.getLogger();
492: if (logger != null)
493: logger.log(this .toString() + ": " + message);
494: else
495: System.out.println(this .toString() + ": " + message);
496:
497: }
498:
499: /**
500: * Log a message on the Logger associated with our Container (if any).
501: *
502: * @param message Message to be logged
503: * @param throwable Associated exception
504: */
505: protected void log(String message, Throwable throwable) {
506:
507: Logger logger = container.getLogger();
508: if (logger != null)
509: logger.log(this .toString() + ": " + message, throwable);
510: else {
511: System.out.println(this .toString() + ": " + message);
512: throwable.printStackTrace(System.out);
513: }
514:
515: }
516:
517: /**
518: * Look up and return the cached SingleSignOn entry associated with this
519: * sso id value, if there is one; otherwise return <code>null</code>.
520: *
521: * @param ssoId Single sign on identifier to look up
522: */
523: protected SingleSignOnEntry lookup(String ssoId) {
524:
525: synchronized (cache) {
526: return ((SingleSignOnEntry) cache.get(ssoId));
527: }
528:
529: }
530:
531: }
532:
533: // ------------------------------------------------------------ Private Classes
534:
535: /**
536: * A private class representing entries in the cache of authenticated users.
537: */
538: class SingleSignOnEntry {
539:
540: public String authType = null;
541:
542: public String password = null;
543:
544: public Principal principal = null;
545:
546: public Session sessions[] = new Session[0];
547:
548: public String username = null;
549:
550: public SingleSignOnEntry(Principal principal, String authType,
551: String username, String password) {
552: super ();
553: this .principal = principal;
554: this .authType = authType;
555: this .username = username;
556: this .password = password;
557: }
558:
559: public synchronized void addSession(SingleSignOn sso,
560: Session session) {
561: for (int i = 0; i < sessions.length; i++) {
562: if (session == sessions[i])
563: return;
564: }
565: Session results[] = new Session[sessions.length + 1];
566: System.arraycopy(sessions, 0, results, 0, sessions.length);
567: results[sessions.length] = session;
568: sessions = results;
569: session.addSessionListener(sso);
570: }
571:
572: public synchronized Session[] findSessions() {
573: return (this.sessions);
574: }
575:
576: }
|