KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > catalina > authenticator > SingleSignOn


1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements. See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License. You may obtain a copy of the License at
8  *
9  * http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17
18
19 package org.apache.catalina.authenticator;
20
21
22 import java.io.IOException JavaDoc;
23 import java.security.Principal JavaDoc;
24 import java.util.HashMap JavaDoc;
25 import java.util.Map JavaDoc;
26
27 import javax.servlet.ServletException JavaDoc;
28 import javax.servlet.http.Cookie JavaDoc;
29
30 import org.apache.catalina.Lifecycle;
31 import org.apache.catalina.LifecycleException;
32 import org.apache.catalina.LifecycleListener;
33 import org.apache.catalina.Realm;
34 import org.apache.catalina.Session;
35 import org.apache.catalina.SessionEvent;
36 import org.apache.catalina.SessionListener;
37 import org.apache.catalina.connector.Request;
38 import org.apache.catalina.connector.Response;
39 import org.apache.catalina.util.LifecycleSupport;
40 import org.apache.catalina.util.StringManager;
41 import org.apache.catalina.valves.ValveBase;
42
43
44 /**
45  * A <strong>Valve</strong> that supports a "single sign on" user experience,
46  * where the security identity of a user who successfully authenticates to one
47  * web application is propogated to other web applications in the same
48  * security domain. For successful use, the following requirements must
49  * be met:
50  * <ul>
51  * <li>This Valve must be configured on the Container that represents a
52  * virtual host (typically an implementation of <code>Host</code>).</li>
53  * <li>The <code>Realm</code> that contains the shared user and role
54  * information must be configured on the same Container (or a higher
55  * one), and not overridden at the web application level.</li>
56  * <li>The web applications themselves must use one of the standard
57  * Authenticators found in the
58  * <code>org.apache.catalina.authenticator</code> package.</li>
59  * </ul>
60  *
61  * @author Craig R. McClanahan
62  * @version $Revision: 467222 $ $Date: 2006-10-24 05:17:11 +0200 (mar., 24 oct. 2006) $
63  */

64
65 public class SingleSignOn
66     extends ValveBase
67     implements Lifecycle, SessionListener {
68
69
70     // ----------------------------------------------------- Instance Variables
71

72
73     /**
74      * The cache of SingleSignOnEntry instances for authenticated Principals,
75      * keyed by the cookie value that is used to select them.
76      */

77     protected Map JavaDoc cache = new HashMap JavaDoc();
78
79
80     /**
81      * Descriptive information about this Valve implementation.
82      */

83     protected static String JavaDoc info =
84         "org.apache.catalina.authenticator.SingleSignOn";
85
86
87     /**
88      * The lifecycle event support for this component.
89      */

90     protected LifecycleSupport lifecycle = new LifecycleSupport(this);
91
92     /**
93      * Indicates whether this valve should require a downstream Authenticator to
94      * reauthenticate each request, or if it itself can bind a UserPrincipal
95      * and AuthType object to the request.
96      */

97     private boolean requireReauthentication = false;
98
99     /**
100      * The cache of single sign on identifiers, keyed by the Session that is
101      * associated with them.
102      */

103     protected Map JavaDoc reverse = new HashMap JavaDoc();
104
105
106     /**
107      * The string manager for this package.
108      */

109     protected final static StringManager sm =
110         StringManager.getManager(Constants.Package);
111
112
113     /**
114      * Component started flag.
115      */

116     protected boolean started = false;
117
118     /**
119      * Optional SSO cookie domain.
120      */

121     private String JavaDoc cookieDomain;
122
123     // ------------------------------------------------------------- Properties
124

125     /**
126      * Returns the optional cookie domain.
127      * May return null.
128      *
129      * @return The cookie domain
130      */

131     public String JavaDoc getCookieDomain() {
132         return cookieDomain;
133     }
134     /**
135      * Sets the domain to be used for sso cookies.
136      *
137      * @param cookieDomain cookie domain name
138      */

139     public void setCookieDomain(String JavaDoc cookieDomain) {
140         if (cookieDomain != null && cookieDomain.trim().length() == 0) {
141             cookieDomain = null;
142         }
143         this.cookieDomain = cookieDomain;
144     }
145
146     /**
147      * Gets whether each request needs to be reauthenticated (by an
148      * Authenticator downstream in the pipeline) to the security
149      * <code>Realm</code>, or if this Valve can itself bind security info
150      * to the request based on the presence of a valid SSO entry without
151      * rechecking with the <code>Realm</code..
152      *
153      * @return <code>true</code> if it is required that a downstream
154      * Authenticator reauthenticate each request before calls to
155      * <code>HttpServletRequest.setUserPrincipal()</code>
156      * and <code>HttpServletRequest.setAuthType()</code> are made;
157      * <code>false</code> if the <code>Valve</code> can itself make
158      * those calls relying on the presence of a valid SingleSignOn
159      * entry associated with the request.
160      *
161      * @see #setRequireReauthentication
162      */

163     public boolean getRequireReauthentication()
164     {
165         return requireReauthentication;
166     }
167
168
169     /**
170      * Sets whether each request needs to be reauthenticated (by an
171      * Authenticator downstream in the pipeline) to the security
172      * <code>Realm</code>, or if this Valve can itself bind security info
173      * to the request, based on the presence of a valid SSO entry, without
174      * rechecking with the <code>Realm</code.
175      * <p>
176      * If this property is <code>false</code> (the default), this
177      * <code>Valve</code> will bind a UserPrincipal and AuthType to the request
178      * if a valid SSO entry is associated with the request. It will not notify
179      * the security <code>Realm</code> of the incoming request.
180      * <p>
181      * This property should be set to <code>true</code> if the overall server
182      * configuration requires that the <code>Realm</code> reauthenticate each
183      * request thread. An example of such a configuration would be one where
184      * the <code>Realm</code> implementation provides security for both a
185      * web tier and an associated EJB tier, and needs to set security
186      * credentials on each request thread in order to support EJB access.
187      * <p>
188      * If this property is set to <code>true</code>, this Valve will set flags
189      * on the request notifying the downstream Authenticator that the request
190      * is associated with an SSO session. The Authenticator will then call its
191      * {@link AuthenticatorBase#reauthenticateFromSSO reauthenticateFromSSO}
192      * method to attempt to reauthenticate the request to the
193      * <code>Realm</code>, using any credentials that were cached with this
194      * Valve.
195      * <p>
196      * The default value of this property is <code>false</code>, in order
197      * to maintain backward compatibility with previous versions of Tomcat.
198      *
199      * @param required <code>true</code> if it is required that a downstream
200      * Authenticator reauthenticate each request before calls
201      * to <code>HttpServletRequest.setUserPrincipal()</code>
202      * and <code>HttpServletRequest.setAuthType()</code> are
203      * made; <code>false</code> if the <code>Valve</code> can
204      * itself make those calls relying on the presence of a
205      * valid SingleSignOn entry associated with the request.
206      *
207      * @see AuthenticatorBase#reauthenticateFromSSO
208      */

209     public void setRequireReauthentication(boolean required)
210     {
211         this.requireReauthentication = required;
212     }
213
214
215     // ------------------------------------------------------ Lifecycle Methods
216

217
218     /**
219      * Add a lifecycle event listener to this component.
220      *
221      * @param listener The listener to add
222      */

223     public void addLifecycleListener(LifecycleListener listener) {
224
225         lifecycle.addLifecycleListener(listener);
226
227     }
228
229
230     /**
231      * Get the lifecycle listeners associated with this lifecycle. If this
232      * Lifecycle has no listeners registered, a zero-length array is returned.
233      */

234     public LifecycleListener[] findLifecycleListeners() {
235
236         return lifecycle.findLifecycleListeners();
237
238     }
239
240
241     /**
242      * Remove a lifecycle event listener from this component.
243      *
244      * @param listener The listener to remove
245      */

246     public void removeLifecycleListener(LifecycleListener listener) {
247
248         lifecycle.removeLifecycleListener(listener);
249
250     }
251
252
253     /**
254      * Prepare for the beginning of active use of the public methods of this
255      * component. This method should be called after <code>configure()</code>,
256      * and before any of the public methods of the component are utilized.
257      *
258      * @exception LifecycleException if this component detects a fatal error
259      * that prevents this component from being used
260      */

261     public void start() throws LifecycleException {
262
263         // Validate and update our current component state
264
if (started)
265             throw new LifecycleException
266                 (sm.getString("authenticator.alreadyStarted"));
267         lifecycle.fireLifecycleEvent(START_EVENT, null);
268         started = true;
269
270     }
271
272
273     /**
274      * Gracefully terminate the active use of the public methods of this
275      * component. This method should be the last one called on a given
276      * instance of this component.
277      *
278      * @exception LifecycleException if this component detects a fatal error
279      * that needs to be reported
280      */

281     public void stop() throws LifecycleException {
282
283         // Validate and update our current component state
284
if (!started)
285             throw new LifecycleException
286                 (sm.getString("authenticator.notStarted"));
287         lifecycle.fireLifecycleEvent(STOP_EVENT, null);
288         started = false;
289
290     }
291
292
293     // ------------------------------------------------ SessionListener Methods
294

295
296     /**
297      * Acknowledge the occurrence of the specified event.
298      *
299      * @param event SessionEvent that has occurred
300      */

301     public void sessionEvent(SessionEvent event) {
302
303         // We only care about session destroyed events
304
if (!Session.SESSION_DESTROYED_EVENT.equals(event.getType())
305                 && (!Session.SESSION_PASSIVATED_EVENT.equals(event.getType())))
306             return;
307
308         // Look up the single session id associated with this session (if any)
309
Session session = event.getSession();
310         if (containerLog.isDebugEnabled())
311             containerLog.debug("Process session destroyed on " + session);
312
313         String JavaDoc ssoId = null;
314         synchronized (reverse) {
315             ssoId = (String JavaDoc) reverse.get(session);
316         }
317         if (ssoId == null)
318             return;
319
320         // Was the session destroyed as the result of a timeout?
321
// If so, we'll just remove the expired session from the
322
// SSO. If the session was logged out, we'll log out
323
// of all session associated with the SSO.
324
if (((session.getMaxInactiveInterval() > 0)
325             && (System.currentTimeMillis() - session.getLastAccessedTimeInternal() >=
326                 session.getMaxInactiveInterval() * 1000))
327             || (Session.SESSION_PASSIVATED_EVENT.equals(event.getType()))) {
328             removeSession(ssoId, session);
329         } else {
330             // The session was logged out.
331
// Deregister this single session id, invalidating
332
// associated sessions
333
deregister(ssoId);
334         }
335
336     }
337
338
339     // ---------------------------------------------------------- Valve Methods
340

341
342     /**
343      * Return descriptive information about this Valve implementation.
344      */

345     public String JavaDoc getInfo() {
346
347         return (info);
348
349     }
350
351
352     /**
353      * Perform single-sign-on support processing for this request.
354      *
355      * @param request The servlet request we are processing
356      * @param response The servlet response we are creating
357      *
358      * @exception IOException if an input/output error occurs
359      * @exception ServletException if a servlet error occurs
360      */

361     public void invoke(Request request, Response response)
362         throws IOException JavaDoc, ServletException JavaDoc {
363
364         request.removeNote(Constants.REQ_SSOID_NOTE);
365
366         // Has a valid user already been authenticated?
367
if (containerLog.isDebugEnabled())
368             containerLog.debug("Process request for '" + request.getRequestURI() + "'");
369         if (request.getUserPrincipal() != null) {
370             if (containerLog.isDebugEnabled())
371                 containerLog.debug(" Principal '" + request.getUserPrincipal().getName() +
372                     "' has already been authenticated");
373             getNext().invoke(request, response);
374             return;
375         }
376
377         // Check for the single sign on cookie
378
if (containerLog.isDebugEnabled())
379             containerLog.debug(" Checking for SSO cookie");
380         Cookie JavaDoc cookie = null;
381         Cookie JavaDoc cookies[] = request.getCookies();
382         if (cookies == null)
383             cookies = new Cookie JavaDoc[0];
384         for (int i = 0; i < cookies.length; i++) {
385             if (Constants.SINGLE_SIGN_ON_COOKIE.equals(cookies[i].getName())) {
386                 cookie = cookies[i];
387                 break;
388             }
389         }
390         if (cookie == null) {
391             if (containerLog.isDebugEnabled())
392                 containerLog.debug(" SSO cookie is not present");
393             getNext().invoke(request, response);
394             return;
395         }
396
397         // Look up the cached Principal associated with this cookie value
398
if (containerLog.isDebugEnabled())
399             containerLog.debug(" Checking for cached principal for " + cookie.getValue());
400         SingleSignOnEntry entry = lookup(cookie.getValue());
401         if (entry != null) {
402             if (containerLog.isDebugEnabled())
403                 containerLog.debug(" Found cached principal '" +
404                     entry.getPrincipal().getName() + "' with auth type '" +
405                     entry.getAuthType() + "'");
406             request.setNote(Constants.REQ_SSOID_NOTE, cookie.getValue());
407             // Only set security elements if reauthentication is not required
408
if (!getRequireReauthentication()) {
409                 request.setAuthType(entry.getAuthType());
410                 request.setUserPrincipal(entry.getPrincipal());
411             }
412         } else {
413             if (containerLog.isDebugEnabled())
414                 containerLog.debug(" No cached principal found, erasing SSO cookie");
415             cookie.setMaxAge(0);
416             response.addCookie(cookie);
417         }
418
419         // Invoke the next Valve in our pipeline
420
getNext().invoke(request, response);
421
422     }
423
424
425     // --------------------------------------------------------- Public Methods
426

427
428     /**
429      * Return a String rendering of this object.
430      */

431     public String JavaDoc toString() {
432
433         StringBuffer JavaDoc sb = new StringBuffer JavaDoc("SingleSignOn[");
434         if (container == null )
435             sb.append("Container is null");
436         else
437             sb.append(container.getName());
438         sb.append("]");
439         return (sb.toString());
440
441     }
442
443
444     // ------------------------------------------------------ Protected Methods
445

446
447     /**
448      * Associate the specified single sign on identifier with the
449      * specified Session.
450      *
451      * @param ssoId Single sign on identifier
452      * @param session Session to be associated
453      */

454     protected void associate(String JavaDoc ssoId, Session session) {
455
456         if (containerLog.isDebugEnabled())
457             containerLog.debug("Associate sso id " + ssoId + " with session " + session);
458
459         SingleSignOnEntry sso = lookup(ssoId);
460         if (sso != null)
461             sso.addSession(this, session);
462         synchronized (reverse) {
463             reverse.put(session, ssoId);
464         }
465
466     }
467
468     /**
469      * Deregister the specified session. If it is the last session,
470      * then also get rid of the single sign on identifier
471      *
472      * @param ssoId Single sign on identifier
473      * @param session Session to be deregistered
474      */

475     protected void deregister(String JavaDoc ssoId, Session session) {
476
477         synchronized (reverse) {
478             reverse.remove(session);
479         }
480
481         SingleSignOnEntry sso = lookup(ssoId);
482         if ( sso == null )
483             return;
484
485         sso.removeSession( session );
486
487         // see if we are the last session, if so blow away ssoId
488
Session sessions[] = sso.findSessions();
489         if ( sessions == null || sessions.length == 0 ) {
490             synchronized (cache) {
491                 sso = (SingleSignOnEntry) cache.remove(ssoId);
492             }
493         }
494
495     }
496
497
498     /**
499      * Deregister the specified single sign on identifier, and invalidate
500      * any associated sessions.
501      *
502      * @param ssoId Single sign on identifier to deregister
503      */

504     protected void deregister(String JavaDoc ssoId) {
505
506         if (containerLog.isDebugEnabled())
507             containerLog.debug("Deregistering sso id '" + ssoId + "'");
508
509         // Look up and remove the corresponding SingleSignOnEntry
510
SingleSignOnEntry sso = null;
511         synchronized (cache) {
512             sso = (SingleSignOnEntry) cache.remove(ssoId);
513         }
514
515         if (sso == null)
516             return;
517
518         // Expire any associated sessions
519
Session sessions[] = sso.findSessions();
520         for (int i = 0; i < sessions.length; i++) {
521             if (containerLog.isTraceEnabled())
522                 containerLog.trace(" Invalidating session " + sessions[i]);
523             // Remove from reverse cache first to avoid recursion
524
synchronized (reverse) {
525                 reverse.remove(sessions[i]);
526             }
527             // Invalidate this session
528
sessions[i].expire();
529         }
530
531         // NOTE: Clients may still possess the old single sign on cookie,
532
// but it will be removed on the next request since it is no longer
533
// in the cache
534

535     }
536
537
538     /**
539      * Attempts reauthentication to the given <code>Realm</code> using
540      * the credentials associated with the single sign-on session
541      * identified by argument <code>ssoId</code>.
542      * <p>
543      * If reauthentication is successful, the <code>Principal</code> and
544      * authorization type associated with the SSO session will be bound
545      * to the given <code>Request</code> object via calls to
546      * {@link Request#setAuthType Request.setAuthType()} and
547      * {@link Request#setUserPrincipal Request.setUserPrincipal()}
548      * </p>
549      *
550      * @param ssoId identifier of SingleSignOn session with which the
551      * caller is associated
552      * @param realm Realm implementation against which the caller is to
553      * be authenticated
554      * @param request the request that needs to be authenticated
555      *
556      * @return <code>true</code> if reauthentication was successful,
557      * <code>false</code> otherwise.
558      */

559     protected boolean reauthenticate(String JavaDoc ssoId, Realm realm,
560                                      Request request) {
561
562         if (ssoId == null || realm == null)
563             return false;
564
565         boolean reauthenticated = false;
566
567         SingleSignOnEntry entry = lookup(ssoId);
568         if (entry != null && entry.getCanReauthenticate()) {
569             
570             String JavaDoc username = entry.getUsername();
571             if (username != null) {
572                 Principal JavaDoc reauthPrincipal =
573                         realm.authenticate(username, entry.getPassword());
574                 if (reauthPrincipal != null) {
575                     reauthenticated = true;
576                     // Bind the authorization credentials to the request
577
request.setAuthType(entry.getAuthType());
578                     request.setUserPrincipal(reauthPrincipal);
579                 }
580             }
581         }
582
583         return reauthenticated;
584     }
585
586
587     /**
588      * Register the specified Principal as being associated with the specified
589      * value for the single sign on identifier.
590      *
591      * @param ssoId Single sign on identifier to register
592      * @param principal Associated user principal that is identified
593      * @param authType Authentication type used to authenticate this
594      * user principal
595      * @param username Username used to authenticate this user
596      * @param password Password used to authenticate this user
597      */

598     protected void register(String JavaDoc ssoId, Principal JavaDoc principal, String JavaDoc authType,
599                   String JavaDoc username, String JavaDoc password) {
600
601         if (containerLog.isDebugEnabled())
602             containerLog.debug("Registering sso id '" + ssoId + "' for user '" +
603                 principal.getName() + "' with auth type '" + authType + "'");
604
605         synchronized (cache) {
606             cache.put(ssoId, new SingleSignOnEntry(principal, authType,
607                                                    username, password));
608         }
609
610     }
611
612
613     /**
614      * Updates any <code>SingleSignOnEntry</code> found under key
615      * <code>ssoId</code> with the given authentication data.
616      * <p>
617      * The purpose of this method is to allow an SSO entry that was
618      * established without a username/password combination (i.e. established
619      * following DIGEST or CLIENT-CERT authentication) to be updated with
620      * a username and password if one becomes available through a subsequent
621      * BASIC or FORM authentication. The SSO entry will then be usable for
622      * reauthentication.
623      * <p>
624      * <b>NOTE:</b> Only updates the SSO entry if a call to
625      * <code>SingleSignOnEntry.getCanReauthenticate()</code> returns
626      * <code>false</code>; otherwise, it is assumed that the SSO entry already
627      * has sufficient information to allow reauthentication and that no update
628      * is needed.
629      *
630      * @param ssoId identifier of Single sign to be updated
631      * @param principal the <code>Principal</code> returned by the latest
632      * call to <code>Realm.authenticate</code>.
633      * @param authType the type of authenticator used (BASIC, CLIENT-CERT,
634      * DIGEST or FORM)
635      * @param username the username (if any) used for the authentication
636      * @param password the password (if any) used for the authentication
637      */

638     protected void update(String JavaDoc ssoId, Principal JavaDoc principal, String JavaDoc authType,
639                           String JavaDoc username, String JavaDoc password) {
640
641         SingleSignOnEntry sso = lookup(ssoId);
642         if (sso != null && !sso.getCanReauthenticate()) {
643             if (containerLog.isDebugEnabled())
644                 containerLog.debug("Update sso id " + ssoId + " to auth type " + authType);
645
646             synchronized(sso) {
647                 sso.updateCredentials(principal, authType, username, password);
648             }
649
650         }
651     }
652
653
654     /**
655      * Look up and return the cached SingleSignOn entry associated with this
656      * sso id value, if there is one; otherwise return <code>null</code>.
657      *
658      * @param ssoId Single sign on identifier to look up
659      */

660     protected SingleSignOnEntry lookup(String JavaDoc ssoId) {
661
662         synchronized (cache) {
663             return ((SingleSignOnEntry) cache.get(ssoId));
664         }
665
666     }
667
668     
669     /**
670      * Remove a single Session from a SingleSignOn. Called when
671      * a session is timed out and no longer active.
672      *
673      * @param ssoId Single sign on identifier from which to remove the session.
674      * @param session the session to be removed.
675      */

676     protected void removeSession(String JavaDoc ssoId, Session session) {
677
678         if (containerLog.isDebugEnabled())
679             containerLog.debug("Removing session " + session.toString() + " from sso id " +
680                 ssoId );
681
682         // Get a reference to the SingleSignOn
683
SingleSignOnEntry entry = lookup(ssoId);
684         if (entry == null)
685             return;
686
687         // Remove the inactive session from SingleSignOnEntry
688
entry.removeSession(session);
689
690         // Remove the inactive session from the 'reverse' Map.
691
synchronized(reverse) {
692             reverse.remove(session);
693         }
694
695         // If there are not sessions left in the SingleSignOnEntry,
696
// deregister the entry.
697
if (entry.findSessions().length == 0) {
698             deregister(ssoId);
699         }
700     }
701
702 }
703
Popular Tags