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