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