001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: package org.apache.catalina.authenticator;
019:
020: import java.io.IOException;
021: import java.security.MessageDigest;
022: import java.security.NoSuchAlgorithmException;
023: import java.security.Principal;
024: import java.text.SimpleDateFormat;
025: import java.util.Date;
026: import java.util.Locale;
027: import java.util.Random;
028:
029: import javax.servlet.ServletException;
030: import javax.servlet.http.Cookie;
031:
032: import org.apache.catalina.Authenticator;
033: import org.apache.catalina.Container;
034: import org.apache.catalina.Context;
035: import org.apache.catalina.Lifecycle;
036: import org.apache.catalina.LifecycleException;
037: import org.apache.catalina.LifecycleListener;
038: import org.apache.catalina.Pipeline;
039: import org.apache.catalina.Realm;
040: import org.apache.catalina.Session;
041: import org.apache.catalina.Valve;
042: import org.apache.catalina.connector.Request;
043: import org.apache.catalina.connector.Response;
044: import org.apache.catalina.deploy.LoginConfig;
045: import org.apache.catalina.deploy.SecurityConstraint;
046: import org.apache.catalina.util.DateTool;
047: import org.apache.catalina.util.LifecycleSupport;
048: import org.apache.catalina.util.StringManager;
049: import org.apache.catalina.valves.ValveBase;
050: import org.apache.juli.logging.Log;
051: import org.apache.juli.logging.LogFactory;
052:
053: /**
054: * Basic implementation of the <b>Valve</b> interface that enforces the
055: * <code><security-constraint></code> elements in the web application
056: * deployment descriptor. This functionality is implemented as a Valve
057: * so that it can be ommitted in environments that do not require these
058: * features. Individual implementations of each supported authentication
059: * method can subclass this base class as required.
060: * <p>
061: * <b>USAGE CONSTRAINT</b>: When this class is utilized, the Context to
062: * which it is attached (or a parent Container in a hierarchy) must have an
063: * associated Realm that can be used for authenticating users and enumerating
064: * the roles to which they have been assigned.
065: * <p>
066: * <b>USAGE CONSTRAINT</b>: This Valve is only useful when processing HTTP
067: * requests. Requests of any other type will simply be passed through.
068: *
069: * @author Craig R. McClanahan
070: * @version $Revision: 500626 $ $Date: 2007-01-27 22:25:41 +0100 (sam., 27 janv. 2007) $
071: */
072:
073: public abstract class AuthenticatorBase extends ValveBase implements
074: Authenticator, Lifecycle {
075: private static Log log = LogFactory.getLog(AuthenticatorBase.class);
076:
077: // ----------------------------------------------------- Instance Variables
078:
079: /**
080: * The default message digest algorithm to use if we cannot use
081: * the requested one.
082: */
083: protected static final String DEFAULT_ALGORITHM = "MD5";
084:
085: /**
086: * The number of random bytes to include when generating a
087: * session identifier.
088: */
089: protected static final int SESSION_ID_BYTES = 16;
090:
091: /**
092: * The message digest algorithm to be used when generating session
093: * identifiers. This must be an algorithm supported by the
094: * <code>java.security.MessageDigest</code> class on your platform.
095: */
096: protected String algorithm = DEFAULT_ALGORITHM;
097:
098: /**
099: * Should we cache authenticated Principals if the request is part of
100: * an HTTP session?
101: */
102: protected boolean cache = true;
103:
104: /**
105: * The Context to which this Valve is attached.
106: */
107: protected Context context = null;
108:
109: /**
110: * Return the MessageDigest implementation to be used when
111: * creating session identifiers.
112: */
113: protected MessageDigest digest = null;
114:
115: /**
116: * A String initialization parameter used to increase the entropy of
117: * the initialization of our random number generator.
118: */
119: protected String entropy = null;
120:
121: /**
122: * Descriptive information about this implementation.
123: */
124: protected static final String info = "org.apache.catalina.authenticator.AuthenticatorBase/1.0";
125:
126: /**
127: * Flag to determine if we disable proxy caching, or leave the issue
128: * up to the webapp developer.
129: */
130: protected boolean disableProxyCaching = true;
131:
132: /**
133: * Flag to determine if we disable proxy caching with headers incompatible
134: * with IE
135: */
136: protected boolean securePagesWithPragma = true;
137:
138: /**
139: * The lifecycle event support for this component.
140: */
141: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
142:
143: /**
144: * A random number generator to use when generating session identifiers.
145: */
146: protected Random random = null;
147:
148: /**
149: * The Java class name of the random number generator class to be used
150: * when generating session identifiers.
151: */
152: protected String randomClass = "java.security.SecureRandom";
153:
154: /**
155: * The string manager for this package.
156: */
157: protected static final StringManager sm = StringManager
158: .getManager(Constants.Package);
159:
160: /**
161: * The SingleSignOn implementation in our request processing chain,
162: * if there is one.
163: */
164: protected SingleSignOn sso = null;
165:
166: /**
167: * Has this component been started?
168: */
169: protected boolean started = false;
170:
171: /**
172: * "Expires" header always set to Date(1), so generate once only
173: */
174: private static final String DATE_ONE = (new SimpleDateFormat(
175: DateTool.HTTP_RESPONSE_DATE_HEADER, Locale.US))
176: .format(new Date(1));
177:
178: // ------------------------------------------------------------- Properties
179:
180: /**
181: * Return the message digest algorithm for this Manager.
182: */
183: public String getAlgorithm() {
184:
185: return (this .algorithm);
186:
187: }
188:
189: /**
190: * Set the message digest algorithm for this Manager.
191: *
192: * @param algorithm The new message digest algorithm
193: */
194: public void setAlgorithm(String algorithm) {
195:
196: this .algorithm = algorithm;
197:
198: }
199:
200: /**
201: * Return the cache authenticated Principals flag.
202: */
203: public boolean getCache() {
204:
205: return (this .cache);
206:
207: }
208:
209: /**
210: * Set the cache authenticated Principals flag.
211: *
212: * @param cache The new cache flag
213: */
214: public void setCache(boolean cache) {
215:
216: this .cache = cache;
217:
218: }
219:
220: /**
221: * Return the Container to which this Valve is attached.
222: */
223: public Container getContainer() {
224:
225: return (this .context);
226:
227: }
228:
229: /**
230: * Set the Container to which this Valve is attached.
231: *
232: * @param container The container to which we are attached
233: */
234: public void setContainer(Container container) {
235:
236: if (!(container instanceof Context))
237: throw new IllegalArgumentException(sm
238: .getString("authenticator.notContext"));
239:
240: super .setContainer(container);
241: this .context = (Context) container;
242:
243: }
244:
245: /**
246: * Return the entropy increaser value, or compute a semi-useful value
247: * if this String has not yet been set.
248: */
249: public String getEntropy() {
250:
251: // Calculate a semi-useful value if this has not been set
252: if (this .entropy == null)
253: setEntropy(this .toString());
254:
255: return (this .entropy);
256:
257: }
258:
259: /**
260: * Set the entropy increaser value.
261: *
262: * @param entropy The new entropy increaser value
263: */
264: public void setEntropy(String entropy) {
265:
266: this .entropy = entropy;
267:
268: }
269:
270: /**
271: * Return descriptive information about this Valve implementation.
272: */
273: public String getInfo() {
274:
275: return (info);
276:
277: }
278:
279: /**
280: * Return the random number generator class name.
281: */
282: public String getRandomClass() {
283:
284: return (this .randomClass);
285:
286: }
287:
288: /**
289: * Set the random number generator class name.
290: *
291: * @param randomClass The new random number generator class name
292: */
293: public void setRandomClass(String randomClass) {
294:
295: this .randomClass = randomClass;
296:
297: }
298:
299: /**
300: * Return the flag that states if we add headers to disable caching by
301: * proxies.
302: */
303: public boolean getDisableProxyCaching() {
304: return disableProxyCaching;
305: }
306:
307: /**
308: * Set the value of the flag that states if we add headers to disable
309: * caching by proxies.
310: * @param nocache <code>true</code> if we add headers to disable proxy
311: * caching, <code>false</code> if we leave the headers alone.
312: */
313: public void setDisableProxyCaching(boolean nocache) {
314: disableProxyCaching = nocache;
315: }
316:
317: /**
318: * Return the flag that states, if proxy caching is disabled, what headers
319: * we add to disable the caching.
320: */
321: public boolean getSecurePagesWithPragma() {
322: return securePagesWithPragma;
323: }
324:
325: /**
326: * Set the value of the flag that states what headers we add to disable
327: * proxy caching.
328: * @param securePagesWithPragma <code>true</code> if we add headers which
329: * are incompatible with downloading office documents in IE under SSL but
330: * which fix a caching problem in Mozilla.
331: */
332: public void setSecurePagesWithPragma(boolean securePagesWithPragma) {
333: this .securePagesWithPragma = securePagesWithPragma;
334: }
335:
336: // --------------------------------------------------------- Public Methods
337:
338: /**
339: * Enforce the security restrictions in the web application deployment
340: * descriptor of our associated Context.
341: *
342: * @param request Request to be processed
343: * @param response Response to be processed
344: *
345: * @exception IOException if an input/output error occurs
346: * @exception ServletException if thrown by a processing element
347: */
348: public void invoke(Request request, Response response)
349: throws IOException, ServletException {
350:
351: if (log.isDebugEnabled())
352: log.debug("Security checking request "
353: + request.getMethod() + " "
354: + request.getRequestURI());
355: LoginConfig config = this .context.getLoginConfig();
356:
357: // Have we got a cached authenticated Principal to record?
358: if (cache) {
359: Principal principal = request.getUserPrincipal();
360: if (principal == null) {
361: Session session = request.getSessionInternal(false);
362: if (session != null) {
363: principal = session.getPrincipal();
364: if (principal != null) {
365: if (log.isDebugEnabled())
366: log.debug("We have cached auth type "
367: + session.getAuthType()
368: + " for principal "
369: + session.getPrincipal());
370: request.setAuthType(session.getAuthType());
371: request.setUserPrincipal(principal);
372: }
373: }
374: }
375: }
376:
377: // Special handling for form-based logins to deal with the case
378: // where the login form (and therefore the "j_security_check" URI
379: // to which it submits) might be outside the secured area
380: String contextPath = this .context.getPath();
381: String requestURI = request.getDecodedRequestURI();
382: if (requestURI.startsWith(contextPath)
383: && requestURI.endsWith(Constants.FORM_ACTION)) {
384: if (!authenticate(request, response, config)) {
385: if (log.isDebugEnabled())
386: log.debug(" Failed authenticate() test ??"
387: + requestURI);
388: return;
389: }
390: }
391:
392: Realm realm = this .context.getRealm();
393: // Is this request URI subject to a security constraint?
394: SecurityConstraint[] constraints = realm
395: .findSecurityConstraints(request, this .context);
396:
397: if ((constraints == null) /* &&
398: (!Constants.FORM_METHOD.equals(config.getAuthMethod())) */) {
399: if (log.isDebugEnabled())
400: log.debug(" Not subject to any constraint");
401: getNext().invoke(request, response);
402: return;
403: }
404:
405: // Make sure that constrained resources are not cached by web proxies
406: // or browsers as caching can provide a security hole
407: if (disableProxyCaching &&
408: // FIXME: Disabled for Mozilla FORM support over SSL
409: // (improper caching issue)
410: //!request.isSecure() &&
411: !"POST".equalsIgnoreCase(request.getMethod())) {
412: if (securePagesWithPragma) {
413: // FIXME: These cause problems with downloading office docs
414: // from IE under SSL and may not be needed for newer Mozilla
415: // clients.
416: response.setHeader("Pragma", "No-cache");
417: response.setHeader("Cache-Control", "no-cache");
418: } else {
419: response.setHeader("Cache-Control", "private");
420: }
421: response.setHeader("Expires", DATE_ONE);
422: }
423:
424: int i;
425: // Enforce any user data constraint for this security constraint
426: if (log.isDebugEnabled()) {
427: log.debug(" Calling hasUserDataPermission()");
428: }
429: if (!realm
430: .hasUserDataPermission(request, response, constraints)) {
431: if (log.isDebugEnabled()) {
432: log.debug(" Failed hasUserDataPermission() test");
433: }
434: /*
435: * ASSERT: Authenticator already set the appropriate
436: * HTTP status code, so we do not have to do anything special
437: */
438: return;
439: }
440:
441: // Since authenticate modifies the response on failure,
442: // we have to check for allow-from-all first.
443: boolean authRequired = true;
444: for (i = 0; i < constraints.length && authRequired; i++) {
445: if (!constraints[i].getAuthConstraint()) {
446: authRequired = false;
447: } else if (!constraints[i].getAllRoles()) {
448: String[] roles = constraints[i].findAuthRoles();
449: if (roles == null || roles.length == 0) {
450: authRequired = false;
451: }
452: }
453: }
454:
455: if (authRequired) {
456: if (log.isDebugEnabled()) {
457: log.debug(" Calling authenticate()");
458: }
459: if (!authenticate(request, response, config)) {
460: if (log.isDebugEnabled()) {
461: log.debug(" Failed authenticate() test");
462: }
463: /*
464: * ASSERT: Authenticator already set the appropriate
465: * HTTP status code, so we do not have to do anything
466: * special
467: */
468: return;
469: }
470: }
471:
472: if (log.isDebugEnabled()) {
473: log.debug(" Calling accessControl()");
474: }
475: if (!realm.hasResourcePermission(request, response,
476: constraints, this .context)) {
477: if (log.isDebugEnabled()) {
478: log.debug(" Failed accessControl() test");
479: }
480: /*
481: * ASSERT: AccessControl method has already set the
482: * appropriate HTTP status code, so we do not have to do
483: * anything special
484: */
485: return;
486: }
487:
488: // Any and all specified constraints have been satisfied
489: if (log.isDebugEnabled()) {
490: log.debug(" Successfully passed all security constraints");
491: }
492: getNext().invoke(request, response);
493:
494: }
495:
496: // ------------------------------------------------------ Protected Methods
497:
498: /**
499: * Associate the specified single sign on identifier with the
500: * specified Session.
501: *
502: * @param ssoId Single sign on identifier
503: * @param session Session to be associated
504: */
505: protected void associate(String ssoId, Session session) {
506:
507: if (sso == null)
508: return;
509: sso.associate(ssoId, session);
510:
511: }
512:
513: /**
514: * Authenticate the user making this request, based on the specified
515: * login configuration. Return <code>true</code> if any specified
516: * constraint has been satisfied, or <code>false</code> if we have
517: * created a response challenge already.
518: *
519: * @param request Request we are processing
520: * @param response Response we are creating
521: * @param config Login configuration describing how authentication
522: * should be performed
523: *
524: * @exception IOException if an input/output error occurs
525: */
526: protected abstract boolean authenticate(Request request,
527: Response response, LoginConfig config) throws IOException;
528:
529: /**
530: * Generate and return a new session identifier for the cookie that
531: * identifies an SSO principal.
532: */
533: protected synchronized String generateSessionId() {
534:
535: // Generate a byte array containing a session identifier
536: byte bytes[] = new byte[SESSION_ID_BYTES];
537: getRandom().nextBytes(bytes);
538: bytes = getDigest().digest(bytes);
539:
540: // Render the result as a String of hexadecimal digits
541: StringBuffer result = new StringBuffer();
542: for (int i = 0; i < bytes.length; i++) {
543: byte b1 = (byte) ((bytes[i] & 0xf0) >> 4);
544: byte b2 = (byte) (bytes[i] & 0x0f);
545: if (b1 < 10)
546: result.append((char) ('0' + b1));
547: else
548: result.append((char) ('A' + (b1 - 10)));
549: if (b2 < 10)
550: result.append((char) ('0' + b2));
551: else
552: result.append((char) ('A' + (b2 - 10)));
553: }
554: return (result.toString());
555:
556: }
557:
558: /**
559: * Return the MessageDigest object to be used for calculating
560: * session identifiers. If none has been created yet, initialize
561: * one the first time this method is called.
562: */
563: protected synchronized MessageDigest getDigest() {
564:
565: if (this .digest == null) {
566: try {
567: this .digest = MessageDigest.getInstance(algorithm);
568: } catch (NoSuchAlgorithmException e) {
569: try {
570: this .digest = MessageDigest
571: .getInstance(DEFAULT_ALGORITHM);
572: } catch (NoSuchAlgorithmException f) {
573: this .digest = null;
574: }
575: }
576: }
577:
578: return (this .digest);
579:
580: }
581:
582: /**
583: * Return the random number generator instance we should use for
584: * generating session identifiers. If there is no such generator
585: * currently defined, construct and seed a new one.
586: */
587: protected synchronized Random getRandom() {
588:
589: if (this .random == null) {
590: try {
591: Class clazz = Class.forName(randomClass);
592: this .random = (Random) clazz.newInstance();
593: long seed = System.currentTimeMillis();
594: char entropy[] = getEntropy().toCharArray();
595: for (int i = 0; i < entropy.length; i++) {
596: long update = ((byte) entropy[i]) << ((i % 8) * 8);
597: seed ^= update;
598: }
599: this .random.setSeed(seed);
600: } catch (Exception e) {
601: this .random = new java.util.Random();
602: }
603: }
604:
605: return (this .random);
606:
607: }
608:
609: /**
610: * Attempts reauthentication to the <code>Realm</code> using
611: * the credentials included in argument <code>entry</code>.
612: *
613: * @param ssoId identifier of SingleSignOn session with which the
614: * caller is associated
615: * @param request the request that needs to be authenticated
616: */
617: protected boolean reauthenticateFromSSO(String ssoId,
618: Request request) {
619:
620: if (sso == null || ssoId == null)
621: return false;
622:
623: boolean reauthenticated = false;
624:
625: Container parent = getContainer();
626: if (parent != null) {
627: Realm realm = parent.getRealm();
628: if (realm != null) {
629: reauthenticated = sso.reauthenticate(ssoId, realm,
630: request);
631: }
632: }
633:
634: if (reauthenticated) {
635: associate(ssoId, request.getSessionInternal(true));
636:
637: if (log.isDebugEnabled()) {
638: log.debug(" Reauthenticated cached principal '"
639: + request.getUserPrincipal().getName()
640: + "' with auth type '" + request.getAuthType()
641: + "'");
642: }
643: }
644:
645: return reauthenticated;
646: }
647:
648: /**
649: * Register an authenticated Principal and authentication type in our
650: * request, in the current session (if there is one), and with our
651: * SingleSignOn valve, if there is one. Set the appropriate cookie
652: * to be returned.
653: *
654: * @param request The servlet request we are processing
655: * @param response The servlet response we are generating
656: * @param principal The authenticated Principal to be registered
657: * @param authType The authentication type to be registered
658: * @param username Username used to authenticate (if any)
659: * @param password Password used to authenticate (if any)
660: */
661: protected void register(Request request, Response response,
662: Principal principal, String authType, String username,
663: String password) {
664:
665: if (log.isDebugEnabled())
666: log.debug("Authenticated '" + principal.getName()
667: + "' with type '" + authType + "'");
668:
669: // Cache the authentication information in our request
670: request.setAuthType(authType);
671: request.setUserPrincipal(principal);
672:
673: Session session = request.getSessionInternal(false);
674: // Cache the authentication information in our session, if any
675: if (cache) {
676: if (session != null) {
677: session.setAuthType(authType);
678: session.setPrincipal(principal);
679: if (username != null)
680: session.setNote(Constants.SESS_USERNAME_NOTE,
681: username);
682: else
683: session.removeNote(Constants.SESS_USERNAME_NOTE);
684: if (password != null)
685: session.setNote(Constants.SESS_PASSWORD_NOTE,
686: password);
687: else
688: session.removeNote(Constants.SESS_PASSWORD_NOTE);
689: }
690: }
691:
692: // Construct a cookie to be returned to the client
693: if (sso == null)
694: return;
695:
696: // Only create a new SSO entry if the SSO did not already set a note
697: // for an existing entry (as it would do with subsequent requests
698: // for DIGEST and SSL authenticated contexts)
699: String ssoId = (String) request
700: .getNote(Constants.REQ_SSOID_NOTE);
701: if (ssoId == null) {
702: // Construct a cookie to be returned to the client
703: ssoId = generateSessionId();
704: Cookie cookie = new Cookie(Constants.SINGLE_SIGN_ON_COOKIE,
705: ssoId);
706: cookie.setMaxAge(-1);
707: cookie.setPath("/");
708:
709: // Bugzilla 41217
710: cookie.setSecure(request.isSecure());
711:
712: // Bugzilla 34724
713: String ssoDomain = sso.getCookieDomain();
714: if (ssoDomain != null) {
715: cookie.setDomain(ssoDomain);
716: }
717:
718: response.addCookie(cookie);
719:
720: // Register this principal with our SSO valve
721: sso
722: .register(ssoId, principal, authType, username,
723: password);
724: request.setNote(Constants.REQ_SSOID_NOTE, ssoId);
725:
726: } else {
727: // Update the SSO session with the latest authentication data
728: sso.update(ssoId, principal, authType, username, password);
729: }
730:
731: // Fix for Bug 10040
732: // Always associate a session with a new SSO reqistration.
733: // SSO entries are only removed from the SSO registry map when
734: // associated sessions are destroyed; if a new SSO entry is created
735: // above for this request and the user never revisits the context, the
736: // SSO entry will never be cleared if we don't associate the session
737: if (session == null)
738: session = request.getSessionInternal(true);
739: sso.associate(ssoId, session);
740:
741: }
742:
743: // ------------------------------------------------------ Lifecycle Methods
744:
745: /**
746: * Add a lifecycle event listener to this component.
747: *
748: * @param listener The listener to add
749: */
750: public void addLifecycleListener(LifecycleListener listener) {
751:
752: lifecycle.addLifecycleListener(listener);
753:
754: }
755:
756: /**
757: * Get the lifecycle listeners associated with this lifecycle. If this
758: * Lifecycle has no listeners registered, a zero-length array is returned.
759: */
760: public LifecycleListener[] findLifecycleListeners() {
761:
762: return lifecycle.findLifecycleListeners();
763:
764: }
765:
766: /**
767: * Remove a lifecycle event listener from this component.
768: *
769: * @param listener The listener to remove
770: */
771: public void removeLifecycleListener(LifecycleListener listener) {
772:
773: lifecycle.removeLifecycleListener(listener);
774:
775: }
776:
777: /**
778: * Prepare for the beginning of active use of the public methods of this
779: * component. This method should be called after <code>configure()</code>,
780: * and before any of the public methods of the component are utilized.
781: *
782: * @exception LifecycleException if this component detects a fatal error
783: * that prevents this component from being used
784: */
785: public void start() throws LifecycleException {
786:
787: // Validate and update our current component state
788: if (started)
789: throw new LifecycleException(sm
790: .getString("authenticator.alreadyStarted"));
791: lifecycle.fireLifecycleEvent(START_EVENT, null);
792: started = true;
793:
794: // Look up the SingleSignOn implementation in our request processing
795: // path, if there is one
796: Container parent = context.getParent();
797: while ((sso == null) && (parent != null)) {
798: if (!(parent instanceof Pipeline)) {
799: parent = parent.getParent();
800: continue;
801: }
802: Valve valves[] = ((Pipeline) parent).getValves();
803: for (int i = 0; i < valves.length; i++) {
804: if (valves[i] instanceof SingleSignOn) {
805: sso = (SingleSignOn) valves[i];
806: break;
807: }
808: }
809: if (sso == null)
810: parent = parent.getParent();
811: }
812: if (log.isDebugEnabled()) {
813: if (sso != null)
814: log.debug("Found SingleSignOn Valve at " + sso);
815: else
816: log.debug("No SingleSignOn Valve is present");
817: }
818:
819: }
820:
821: /**
822: * Gracefully terminate the active use of the public methods of this
823: * component. This method should be the last one called on a given
824: * instance of this component.
825: *
826: * @exception LifecycleException if this component detects a fatal error
827: * that needs to be reported
828: */
829: public void stop() throws LifecycleException {
830:
831: // Validate and update our current component state
832: if (!started)
833: throw new LifecycleException(sm
834: .getString("authenticator.notStarted"));
835: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
836: started = false;
837:
838: sso = null;
839:
840: }
841:
842: }
|