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.realm;
20
21
22 import java.security.Principal;
23 import java.util.ArrayList;
24 import java.util.Iterator;
25 import java.util.List;
26
27 import javax.security.auth.Subject;
28 import javax.security.auth.login.AccountExpiredException;
29 import javax.security.auth.login.CredentialExpiredException;
30 import javax.security.auth.login.FailedLoginException;
31 import javax.security.auth.login.LoginContext;
32 import javax.security.auth.login.LoginException;
33
34 import org.apache.catalina.Container;
35 import org.apache.catalina.LifecycleException;
36 import org.apache.catalina.util.StringManager;
37 import org.apache.juli.logging.Log;
38 import org.apache.juli.logging.LogFactory;
39
40
41 /**
42 * <p>Implmentation of <b>Realm</b> that authenticates users via the <em>Java
43 * Authentication and Authorization Service</em> (JAAS). JAAS support requires
44 * either JDK 1.4 (which includes it as part of the standard platform) or
45 * JDK 1.3 (with the plug-in <code>jaas.jar</code> file).</p>
46 *
47 * <p>The value configured for the <code>appName</code> property is passed to
48 * the <code>javax.security.auth.login.LoginContext</code> constructor, to
49 * specify the <em>application name</em> used to select the set of relevant
50 * <code>LoginModules</code> required.</p>
51 *
52 * <p>The JAAS Specification describes the result of a successful login as a
53 * <code>javax.security.auth.Subject</code> instance, which can contain zero
54 * or more <code>java.security.Principal</code> objects in the return value
55 * of the <code>Subject.getPrincipals()</code> method. However, it provides
56 * no guidance on how to distinguish Principals that describe the individual
57 * user (and are thus appropriate to return as the value of
58 * request.getUserPrincipal() in a web application) from the Principal(s)
59 * that describe the authorized roles for this user. To maintain as much
60 * independence as possible from the underlying <code>LoginMethod</code>
61 * implementation executed by JAAS, the following policy is implemented by
62 * this Realm:</p>
63 * <ul>
64 * <li>The JAAS <code>LoginModule</code> is assumed to return a
65 * <code>Subject</code> with at least one <code>Principal</code> instance
66 * representing the user himself or herself, and zero or more separate
67 * <code>Principals</code> representing the security roles authorized
68 * for this user.</li>
69 * <li>On the <code>Principal</code> representing the user, the Principal
70 * name is an appropriate value to return via the Servlet API method
71 * <code>HttpServletRequest.getRemoteUser()</code>.</li>
72 * <li>On the <code>Principals</code> representing the security roles, the
73 * name is the name of the authorized security role.</li>
74 * <li>This Realm will be configured with two lists of fully qualified Java
75 * class names of classes that implement
76 * <code>java.security.Principal</code> - one that identifies class(es)
77 * representing a user, and one that identifies class(es) representing
78 * a security role.</li>
79 * <li>As this Realm iterates over the <code>Principals</code> returned by
80 * <code>Subject.getPrincipals()</code>, it will identify the first
81 * <code>Principal</code> that matches the "user classes" list as the
82 * <code>Principal</code> for this user.</li>
83 * <li>As this Realm iterates over the <code>Princpals</code> returned by
84 * <code>Subject.getPrincipals()</code>, it will accumulate the set of
85 * all <code>Principals</code> matching the "role classes" list as
86 * identifying the security roles for this user.</li>
87 * <li>It is a configuration error for the JAAS login method to return a
88 * validated <code>Subject</code> without a <code>Principal</code> that
89 * matches the "user classes" list.</li>
90 * <li>By default, the enclosing Container's name serves as the
91 * application name used to obtain the JAAS LoginContext ("Catalina" in
92 * a default installation). Tomcat must be able to find an application
93 * with this name in the JAAS configuration file. Here is a hypothetical
94 * JAAS configuration file entry for a database-oriented login module that uses
95 * a Tomcat-managed JNDI database resource:
96 * <blockquote><pre>Catalina {
97 org.foobar.auth.DatabaseLoginModule REQUIRED
98 JNDI_RESOURCE=jdbc/AuthDB
99 USER_TABLE=users
100 USER_ID_COLUMN=id
101 USER_NAME_COLUMN=name
102 USER_CREDENTIAL_COLUMN=password
103 ROLE_TABLE=roles
104 ROLE_NAME_COLUMN=name
105 PRINCIPAL_FACTORY=org.foobar.auth.impl.SimplePrincipalFactory;
106 };</pre></blockquote></li>
107 * <li>To set the JAAS configuration file
108 * location, set the <code>CATALINA_OPTS</code> environment variable
109 * similar to the following:
110 <blockquote><code>CATALINA_OPTS="-Djava.security.auth.login.config=$CATALINA_HOME/conf/jaas.config"</code></blockquote>
111 * </li>
112 * <li>As part of the login process, JAASRealm registers its own <code>CallbackHandler</code>,
113 * called (unsurprisingly) <code>JAASCallbackHandler</code>. This handler supplies the
114 * HTTP requests's username and credentials to the user-supplied <code>LoginModule</code></li>
115 * <li>As with other <code>Realm</code> implementations, digested passwords are supported if
116 * the <code><Realm></code> element in <code>server.xml</code> contains a
117 * <code>digest</code> attribute; <code>JAASCallbackHandler</code> will digest the password
118 * prior to passing it back to the <code>LoginModule</code></li>
119 * </ul>
120 *
121 * @author Craig R. McClanahan
122 * @author Yoav Shapira
123 * @version $Revision: 607339 $ $Date: 2007-12-28 22:31:46 +0100 (ven., 28 déc. 2007) $
124 */
125
126 public class JAASRealm
127 extends RealmBase
128 {
129 private static Log log = LogFactory.getLog(JAASRealm.class);
130
131 // ----------------------------------------------------- Instance Variables
132
133
134 /**
135 * The application name passed to the JAAS <code>LoginContext</code>,
136 * which uses it to select the set of relevant <code>LoginModule</code>s.
137 */
138 protected String appName = null;
139
140
141 /**
142 * Descriptive information about this <code>Realm</code> implementation.
143 */
144 protected static final String info =
145 "org.apache.catalina.realm.JAASRealm/1.0";
146
147
148 /**
149 * Descriptive information about this <code>Realm</code> implementation.
150 */
151 protected static final String name = "JAASRealm";
152
153
154 /**
155 * The list of role class names, split out for easy processing.
156 */
157 protected List<String> roleClasses = new ArrayList<String>();
158
159
160 /**
161 * The string manager for this package.
162 */
163 protected static final StringManager sm =
164 StringManager.getManager(Constants.Package);
165
166
167 /**
168 * The set of user class names, split out for easy processing.
169 */
170 protected List<String> userClasses = new ArrayList<String>();
171
172
173 /**
174 * Whether to use context ClassLoader or default ClassLoader.
175 * True means use context ClassLoader, and True is the default
176 * value.
177 */
178 protected boolean useContextClassLoader = true;
179
180
181 // ------------------------------------------------------------- Properties
182
183
184 /**
185 * setter for the <code>appName</code> member variable
186 * @deprecated JAAS should use the <code>Engine</code> (domain) name and webpp/host overrides
187 */
188 public void setAppName(String name) {
189 appName = name;
190 }
191
192 /**
193 * getter for the <code>appName</code> member variable
194 */
195 public String getAppName() {
196 return appName;
197 }
198
199 /**
200 * Sets whether to use the context or default ClassLoader.
201 * True means use context ClassLoader.
202 *
203 * @param useContext True means use context ClassLoader
204 */
205 public void setUseContextClassLoader(boolean useContext) {
206 useContextClassLoader = useContext;
207 log.info("Setting useContextClassLoader = " + useContext);
208 }
209
210 /**
211 * Returns whether to use the context or default ClassLoader.
212 * True means to use the context ClassLoader.
213 *
214 * @return The value of useContextClassLoader
215 */
216 public boolean isUseContextClassLoader() {
217 return useContextClassLoader;
218 }
219
220 public void setContainer(Container container) {
221 super.setContainer(container);
222
223 if( appName==null ) {
224 String name=container.getName();
225 name = makeLegalForJAAS(name);
226
227 appName=name;
228
229 log.info("Set JAAS app name " + appName);
230 }
231 }
232
233 /**
234 * Comma-delimited list of <code>java.security.Principal</code> classes
235 * that represent security roles.
236 */
237 protected String roleClassNames = null;
238
239 public String getRoleClassNames() {
240 return (this.roleClassNames);
241 }
242
243 /**
244 * Sets the list of comma-delimited classes that represent roles. The
245 * classes in the list must implement <code>java.security.Principal</code>.
246 * The supplied list of classes will be parsed when {@link #start()} is
247 * called.
248 */
249 public void setRoleClassNames(String roleClassNames) {
250 this.roleClassNames = roleClassNames;
251 }
252
253 /**
254 * Parses a comma-delimited list of class names, and store the class names
255 * in the provided List. Each class must implement
256 * <code>java.security.Principal</code>.
257 *
258 * @param classNamesString a comma-delimited list of fully qualified class names.
259 * @param classNamesList the list in which the class names will be stored.
260 * The list is cleared before being populated.
261 */
262 protected void parseClassNames(String classNamesString, List<String> classNamesList) {
263 classNamesList.clear();
264 if (classNamesString == null) return;
265
266 ClassLoader loader = this.getClass().getClassLoader();
267 if (isUseContextClassLoader())
268 loader = Thread.currentThread().getContextClassLoader();
269
270 String[] classNames = classNamesString.split("[ ]*,[ ]*");
271 for (int i=0; i<classNames.length; i++) {
272 if (classNames[i].length()==0) continue;
273 try {
274 Class principalClass = Class.forName(classNames[i], false,
275 loader);
276 if (Principal.class.isAssignableFrom(principalClass)) {
277 classNamesList.add(classNames[i]);
278 } else {
279 log.error("Class "+classNames[i]+" is not implementing "+
280 "java.security.Principal! Class not added.");
281 }
282 } catch (ClassNotFoundException e) {
283 log.error("Class "+classNames[i]+" not found! Class not added.");
284 }
285 }
286 }
287
288 /**
289 * Comma-delimited list of <code>java.security.Principal</code> classes
290 * that represent individual users.
291 */
292 protected String userClassNames = null;
293
294 public String getUserClassNames() {
295 return (this.userClassNames);
296 }
297
298 /**
299 * Sets the list of comma-delimited classes that represent individual
300 * users. The classes in the list must implement
301 * <code>java.security.Principal</code>. The supplied list of classes will
302 * be parsed when {@link #start()} is called.
303 */
304 public void setUserClassNames(String userClassNames) {
305 this.userClassNames = userClassNames;
306 }
307
308
309 // --------------------------------------------------------- Public Methods
310
311
312 /**
313 * Return the <code>Principal</code> associated with the specified username and
314 * credentials, if there is one; otherwise return <code>null</code>.
315 *
316 * If there are any errors with the JDBC connection, executing
317 * the query or anything we return null (don't authenticate). This
318 * event is also logged, and the connection will be closed so that
319 * a subsequent request will automatically re-open it.
320 *
321 * @param username Username of the <code>Principal</code> to look up
322 * @param credentials Password or other credentials to use in
323 * authenticating this username
324 */
325 public Principal authenticate(String username, String credentials) {
326
327 // Establish a LoginContext to use for authentication
328 try {
329 LoginContext loginContext = null;
330 if( appName==null ) appName="Tomcat";
331
332 if( log.isDebugEnabled())
333 log.debug(sm.getString("jaasRealm.beginLogin", username, appName));
334
335 // What if the LoginModule is in the container class loader ?
336 ClassLoader ocl = null;
337
338 if (!isUseContextClassLoader()) {
339 ocl = Thread.currentThread().getContextClassLoader();
340 Thread.currentThread().setContextClassLoader(
341 this.getClass().getClassLoader());
342 }
343
344 try {
345 loginContext = new LoginContext
346 (appName, new JAASCallbackHandler(this, username,
347 credentials));
348 } catch (Throwable e) {
349 log.error(sm.getString("jaasRealm.unexpectedError"), e);
350 return (null);
351 } finally {
352 if(!isUseContextClassLoader()) {
353 Thread.currentThread().setContextClassLoader(ocl);
354 }
355 }
356
357 if( log.isDebugEnabled())
358 log.debug("Login context created " + username);
359
360 // Negotiate a login via this LoginContext
361 Subject subject = null;
362 try {
363 loginContext.login();
364 subject = loginContext.getSubject();
365 if (subject == null) {
366 if( log.isDebugEnabled())
367 log.debug(sm.getString("jaasRealm.failedLogin", username));
368 return (null);
369 }
370 } catch (AccountExpiredException e) {
371 if (log.isDebugEnabled())
372 log.debug(sm.getString("jaasRealm.accountExpired", username));
373 return (null);
374 } catch (CredentialExpiredException e) {
375 if (log.isDebugEnabled())
376 log.debug(sm.getString("jaasRealm.credentialExpired", username));
377 return (null);
378 } catch (FailedLoginException e) {
379 if (log.isDebugEnabled())
380 log.debug(sm.getString("jaasRealm.failedLogin", username));
381 return (null);
382 } catch (LoginException e) {
383 log.warn(sm.getString("jaasRealm.loginException", username), e);
384 return (null);
385 } catch (Throwable e) {
386 log.error(sm.getString("jaasRealm.unexpectedError"), e);
387 return (null);
388 }
389
390 if( log.isDebugEnabled())
391 log.debug(sm.getString("jaasRealm.loginContextCreated", username));
392
393 // Return the appropriate Principal for this authenticated Subject
394 Principal principal = createPrincipal(username, subject);
395 if (principal == null) {
396 log.debug(sm.getString("jaasRealm.authenticateFailure", username));
397 return (null);
398 }
399 if (log.isDebugEnabled()) {
400 log.debug(sm.getString("jaasRealm.authenticateSuccess", username));
401 }
402
403 return (principal);
404 } catch( Throwable t) {
405 log.error( "error ", t);
406 return null;
407 }
408 }
409
410
411 // -------------------------------------------------------- Package Methods
412
413
414 // ------------------------------------------------------ Protected Methods
415
416
417 /**
418 * Return a short name for this <code>Realm</code> implementation.
419 */
420 protected String getName() {
421
422 return (name);
423
424 }
425
426
427 /**
428 * Return the password associated with the given principal's user name.
429 */
430 protected String getPassword(String username) {
431
432 return (null);
433
434 }
435
436
437 /**
438 * Return the <code>Principal</code> associated with the given user name.
439 */
440 protected Principal getPrincipal(String username) {
441
442 return (null);
443
444 }
445
446
447 /**
448 * Identify and return a <code>java.security.Principal</code> instance
449 * representing the authenticated user for the specified <code>Subject</code>.
450 * The Principal is constructed by scanning the list of Principals returned
451 * by the JAASLoginModule. The first <code>Principal</code> object that matches
452 * one of the class names supplied as a "user class" is the user Principal.
453 * This object is returned to tha caller.
454 * Any remaining principal objects returned by the LoginModules are mapped to
455 * roles, but only if their respective classes match one of the "role class" classes.
456 * If a user Principal cannot be constructed, return <code>null</code>.
457 * @param subject The <code>Subject</code> representing the logged-in user
458 */
459 protected Principal createPrincipal(String username, Subject subject) {
460 // Prepare to scan the Principals for this Subject
461
462 List<String> roles = new ArrayList<String>();
463 Principal userPrincipal = null;
464
465 // Scan the Principals for this Subject
466 Iterator<Principal> principals = subject.getPrincipals().iterator();
467 while (principals.hasNext()) {
468 Principal principal = principals.next();
469
470 String principalClass = principal.getClass().getName();
471
472 if( log.isDebugEnabled() ) {
473 log.debug(sm.getString("jaasRealm.checkPrincipal", principal, principalClass));
474 }
475
476 if (userPrincipal == null && userClasses.contains(principalClass)) {
477 userPrincipal = principal;
478 if( log.isDebugEnabled() ) {
479 log.debug(sm.getString("jaasRealm.userPrincipalSuccess", principal.getName()));
480 }
481 }
482
483 if (roleClasses.contains(principalClass)) {
484 roles.add(principal.getName());
485 if( log.isDebugEnabled() ) {
486 log.debug(sm.getString("jaasRealm.rolePrincipalAdd", principal.getName()));
487 }
488 }
489 }
490
491 // Print failure message if needed
492 if (userPrincipal == null) {
493 if (log.isDebugEnabled()) {
494 log.debug(sm.getString("jaasRealm.userPrincipalFailure"));
495 log.debug(sm.getString("jaasRealm.rolePrincipalFailure"));
496 }
497 } else {
498 if (roles.size() == 0) {
499 if (log.isDebugEnabled()) {
500 log.debug(sm.getString("jaasRealm.rolePrincipalFailure"));
501 }
502 }
503 }
504
505 // Return the resulting Principal for our authenticated user
506 return new GenericPrincipal(this, username, null, roles, userPrincipal);
507 }
508
509 /**
510 * Ensure the given name is legal for JAAS configuration.
511 * Added for Bugzilla 30869, made protected for easy customization
512 * in case my implementation is insufficient, which I think is
513 * very likely.
514 *
515 * @param src The name to validate
516 * @return A string that's a valid JAAS realm name
517 */
518 protected String makeLegalForJAAS(final String src) {
519 String result = src;
520
521 // Default name is "other" per JAAS spec
522 if(result == null) {
523 result = "other";
524 }
525
526 // Strip leading slash if present, as Sun JAAS impl
527 // barfs on it (see Bugzilla 30869 bug report).
528 if(result.startsWith("/")) {
529 result = result.substring(1);
530 }
531
532 return result;
533 }
534
535
536 // ------------------------------------------------------ Lifecycle Methods
537
538
539 /**
540 *
541 * Prepare for active use of the public methods of this <code>Component</code>.
542 *
543 * @exception LifecycleException if this component detects a fatal error
544 * that prevents it from being started
545 */
546 public void start() throws LifecycleException {
547
548 // Perform normal superclass initialization
549 super.start();
550
551 // These need to be called after loading configuration, in case
552 // useContextClassLoader appears after them in xml config
553 parseClassNames(userClassNames, userClasses);
554 parseClassNames(roleClassNames, roleClasses);
555 }
556
557
558 /**
559 * Gracefully shut down active use of the public methods of this <code>Component</code>.
560 *
561 * @exception LifecycleException if this component detects a fatal error
562 * that needs to be reported
563 */
564 public void stop() throws LifecycleException {
565
566 // Perform normal superclass finalization
567 super.stop();
568
569 }
570
571
572 }