| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.catalina.realm; |
| |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.InvocationTargetException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.security.Principal; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import javax.security.auth.Subject; |
| import javax.security.auth.callback.CallbackHandler; |
| import javax.security.auth.login.AccountExpiredException; |
| import javax.security.auth.login.Configuration; |
| import javax.security.auth.login.CredentialExpiredException; |
| import javax.security.auth.login.FailedLoginException; |
| import javax.security.auth.login.LoginContext; |
| import javax.security.auth.login.LoginException; |
| import javax.servlet.http.HttpServletRequest; |
| |
| import org.apache.catalina.Container; |
| import org.apache.catalina.LifecycleException; |
| import org.apache.juli.logging.Log; |
| import org.apache.juli.logging.LogFactory; |
| import org.apache.tomcat.util.ExceptionUtils; |
| |
| /** |
| * <p>Implementation of <b>Realm</b> that authenticates users via the <em>Java |
| * Authentication and Authorization Service</em> (JAAS). JAAS support requires |
| * either JDK 1.4 (which includes it as part of the standard platform) or |
| * JDK 1.3 (with the plug-in <code>jaas.jar</code> file).</p> |
| * |
| * <p>The value configured for the <code>appName</code> property is passed to |
| * the <code>javax.security.auth.login.LoginContext</code> constructor, to |
| * specify the <em>application name</em> used to select the set of relevant |
| * <code>LoginModules</code> required.</p> |
| * |
| * <p>The JAAS Specification describes the result of a successful login as a |
| * <code>javax.security.auth.Subject</code> instance, which can contain zero |
| * or more <code>java.security.Principal</code> objects in the return value |
| * of the <code>Subject.getPrincipals()</code> method. However, it provides |
| * no guidance on how to distinguish Principals that describe the individual |
| * user (and are thus appropriate to return as the value of |
| * request.getUserPrincipal() in a web application) from the Principal(s) |
| * that describe the authorized roles for this user. To maintain as much |
| * independence as possible from the underlying <code>LoginMethod</code> |
| * implementation executed by JAAS, the following policy is implemented by |
| * this Realm:</p> |
| * <ul> |
| * <li>The JAAS <code>LoginModule</code> is assumed to return a |
| * <code>Subject</code> with at least one <code>Principal</code> instance |
| * representing the user himself or herself, and zero or more separate |
| * <code>Principals</code> representing the security roles authorized |
| * for this user.</li> |
| * <li>On the <code>Principal</code> representing the user, the Principal |
| * name is an appropriate value to return via the Servlet API method |
| * <code>HttpServletRequest.getRemoteUser()</code>.</li> |
| * <li>On the <code>Principals</code> representing the security roles, the |
| * name is the name of the authorized security role.</li> |
| * <li>This Realm will be configured with two lists of fully qualified Java |
| * class names of classes that implement |
| * <code>java.security.Principal</code> - one that identifies class(es) |
| * representing a user, and one that identifies class(es) representing |
| * a security role.</li> |
| * <li>As this Realm iterates over the <code>Principals</code> returned by |
| * <code>Subject.getPrincipals()</code>, it will identify the first |
| * <code>Principal</code> that matches the "user classes" list as the |
| * <code>Principal</code> for this user.</li> |
| * <li>As this Realm iterates over the <code>Principals</code> returned by |
| * <code>Subject.getPrincipals()</code>, it will accumulate the set of |
| * all <code>Principals</code> matching the "role classes" list as |
| * identifying the security roles for this user.</li> |
| * <li>It is a configuration error for the JAAS login method to return a |
| * validated <code>Subject</code> without a <code>Principal</code> that |
| * matches the "user classes" list.</li> |
| * <li>By default, the enclosing Container's name serves as the |
| * application name used to obtain the JAAS LoginContext ("Catalina" in |
| * a default installation). Tomcat must be able to find an application |
| * with this name in the JAAS configuration file. Here is a hypothetical |
| * JAAS configuration file entry for a database-oriented login module that uses |
| * a Tomcat-managed JNDI database resource: |
| * <blockquote><pre>Catalina { |
| org.foobar.auth.DatabaseLoginModule REQUIRED |
| JNDI_RESOURCE=jdbc/AuthDB |
| USER_TABLE=users |
| USER_ID_COLUMN=id |
| USER_NAME_COLUMN=name |
| USER_CREDENTIAL_COLUMN=password |
| ROLE_TABLE=roles |
| ROLE_NAME_COLUMN=name |
| PRINCIPAL_FACTORY=org.foobar.auth.impl.SimplePrincipalFactory; |
| };</pre></blockquote></li> |
| * <li>To set the JAAS configuration file |
| * location, set the <code>CATALINA_OPTS</code> environment variable |
| * similar to the following: |
| <blockquote><code>CATALINA_OPTS="-Djava.security.auth.login.config=$CATALINA_HOME/conf/jaas.config"</code></blockquote> |
| * </li> |
| * <li>As part of the login process, JAASRealm registers its own <code>CallbackHandler</code>, |
| * called (unsurprisingly) <code>JAASCallbackHandler</code>. This handler supplies the |
| * HTTP requests's username and credentials to the user-supplied <code>LoginModule</code></li> |
| * <li>As with other <code>Realm</code> implementations, digested passwords are supported if |
| * the <code><Realm></code> element in <code>server.xml</code> contains a |
| * <code>digest</code> attribute; <code>JAASCallbackHandler</code> will digest the password |
| * prior to passing it back to the <code>LoginModule</code></li> |
| * </ul> |
| * |
| * @author Craig R. McClanahan |
| * @author Yoav Shapira |
| * @version $Id$ |
| */ |
| |
| public class JAASRealm extends RealmBase { |
| |
| private static final Log log = LogFactory.getLog(JAASRealm.class); |
| |
| // ----------------------------------------------------- Instance Variables |
| |
| |
| /** |
| * The application name passed to the JAAS <code>LoginContext</code>, |
| * which uses it to select the set of relevant <code>LoginModule</code>s. |
| */ |
| protected String appName = null; |
| |
| |
| /** |
| * Descriptive information about this <code>Realm</code> implementation. |
| */ |
| protected static final String name = "JAASRealm"; |
| |
| |
| /** |
| * The list of role class names, split out for easy processing. |
| */ |
| protected final List<String> roleClasses = new ArrayList<>(); |
| |
| |
| /** |
| * The set of user class names, split out for easy processing. |
| */ |
| protected final List<String> userClasses = new ArrayList<>(); |
| |
| |
| /** |
| * Whether to use context ClassLoader or default ClassLoader. |
| * True means use context ClassLoader, and True is the default |
| * value. |
| */ |
| protected boolean useContextClassLoader = true; |
| |
| |
| /** |
| * Path to find a JAAS configuration file, if not set global JVM JAAS |
| * configuration will be used. |
| */ |
| protected String configFile; |
| |
| protected Configuration jaasConfiguration; |
| protected volatile boolean jaasConfigurationLoaded = false; |
| |
| |
| // ------------------------------------------------------------- Properties |
| |
| /** |
| * Getter for the <code>configfile</code> member variable. |
| */ |
| public String getConfigFile() { |
| return configFile; |
| } |
| |
| /** |
| * Setter for the <code>configfile</code> member variable. |
| */ |
| public void setConfigFile(String configFile) { |
| this.configFile = configFile; |
| } |
| |
| /** |
| * setter for the <code>appName</code> member variable |
| */ |
| public void setAppName(String name) { |
| appName = name; |
| } |
| |
| /** |
| * getter for the <code>appName</code> member variable |
| */ |
| public String getAppName() { |
| return appName; |
| } |
| |
| /** |
| * Sets whether to use the context or default ClassLoader. |
| * True means use context ClassLoader. |
| * |
| * @param useContext True means use context ClassLoader |
| */ |
| public void setUseContextClassLoader(boolean useContext) { |
| useContextClassLoader = useContext; |
| log.info("Setting useContextClassLoader = " + useContext); |
| } |
| |
| /** |
| * Returns whether to use the context or default ClassLoader. |
| * True means to use the context ClassLoader. |
| * |
| * @return The value of useContextClassLoader |
| */ |
| public boolean isUseContextClassLoader() { |
| return useContextClassLoader; |
| } |
| |
| @Override |
| public void setContainer(Container container) { |
| super.setContainer(container); |
| |
| if( appName==null ) { |
| String name = container.getName(); |
| if (!name.startsWith("/")) { |
| name = "/" + name; |
| } |
| name = makeLegalForJAAS(name); |
| |
| appName=name; |
| |
| log.info("Set JAAS app name " + appName); |
| } |
| } |
| |
| /** |
| * Comma-delimited list of <code>java.security.Principal</code> classes |
| * that represent security roles. |
| */ |
| protected String roleClassNames = null; |
| |
| public String getRoleClassNames() { |
| return (this.roleClassNames); |
| } |
| |
| /** |
| * Sets the list of comma-delimited classes that represent roles. The |
| * classes in the list must implement <code>java.security.Principal</code>. |
| * The supplied list of classes will be parsed when {@link #start()} is |
| * called. |
| */ |
| public void setRoleClassNames(String roleClassNames) { |
| this.roleClassNames = roleClassNames; |
| } |
| |
| /** |
| * Parses a comma-delimited list of class names, and store the class names |
| * in the provided List. Each class must implement |
| * <code>java.security.Principal</code>. |
| * |
| * @param classNamesString a comma-delimited list of fully qualified class names. |
| * @param classNamesList the list in which the class names will be stored. |
| * The list is cleared before being populated. |
| */ |
| protected void parseClassNames(String classNamesString, List<String> classNamesList) { |
| classNamesList.clear(); |
| if (classNamesString == null) return; |
| |
| ClassLoader loader = this.getClass().getClassLoader(); |
| if (isUseContextClassLoader()) |
| loader = Thread.currentThread().getContextClassLoader(); |
| |
| String[] classNames = classNamesString.split("[ ]*,[ ]*"); |
| for (int i=0; i<classNames.length; i++) { |
| if (classNames[i].length()==0) continue; |
| try { |
| Class<?> principalClass = Class.forName(classNames[i], false, |
| loader); |
| if (Principal.class.isAssignableFrom(principalClass)) { |
| classNamesList.add(classNames[i]); |
| } else { |
| log.error("Class "+classNames[i]+" is not implementing "+ |
| "java.security.Principal! Class not added."); |
| } |
| } catch (ClassNotFoundException e) { |
| log.error("Class "+classNames[i]+" not found! Class not added."); |
| } |
| } |
| } |
| |
| /** |
| * Comma-delimited list of <code>java.security.Principal</code> classes |
| * that represent individual users. |
| */ |
| protected String userClassNames = null; |
| |
| public String getUserClassNames() { |
| return (this.userClassNames); |
| } |
| |
| /** |
| * Sets the list of comma-delimited classes that represent individual |
| * users. The classes in the list must implement |
| * <code>java.security.Principal</code>. The supplied list of classes will |
| * be parsed when {@link #start()} is called. |
| */ |
| public void setUserClassNames(String userClassNames) { |
| this.userClassNames = userClassNames; |
| } |
| |
| |
| // --------------------------------------------------------- Public Methods |
| |
| /** |
| * Return the <code>Principal</code> associated with the specified username |
| * and credentials, if there is one; otherwise return <code>null</code>. |
| * |
| * @param username Username of the <code>Principal</code> to look up |
| * @param credentials Password or other credentials to use in |
| * authenticating this username |
| */ |
| @Override |
| public Principal authenticate(String username, String credentials) { |
| return authenticate(username, |
| new JAASCallbackHandler(this, username, credentials)); |
| } |
| |
| |
| /** |
| * Return the <code>Principal</code> associated with the specified username |
| * and digest, if there is one; otherwise return <code>null</code>. |
| * |
| * @param username Username of the <code>Principal</code> to look up |
| * @param clientDigest Digest to use in authenticating this username |
| * @param nonce Server generated nonce |
| * @param nc Nonce count |
| * @param cnonce Client generated nonce |
| * @param qop Quality of protection applied to the message |
| * @param realmName Realm name |
| * @param md5a2 Second MD5 digest used to calculate the digest |
| * MD5(Method + ":" + uri) |
| */ |
| @Override |
| public Principal authenticate(String username, String clientDigest, |
| String nonce, String nc, String cnonce, String qop, |
| String realmName, String md5a2) { |
| return authenticate(username, |
| new JAASCallbackHandler(this, username, clientDigest, nonce, |
| nc, cnonce, qop, realmName, md5a2, |
| HttpServletRequest.DIGEST_AUTH)); |
| } |
| |
| |
| // -------------------------------------------------------- Package Methods |
| |
| |
| // ------------------------------------------------------ Protected Methods |
| |
| |
| /** |
| * Perform the actual JAAS authentication |
| */ |
| protected Principal authenticate(String username, |
| CallbackHandler callbackHandler) { |
| |
| // Establish a LoginContext to use for authentication |
| try { |
| LoginContext loginContext = null; |
| if( appName==null ) appName="Tomcat"; |
| |
| if( log.isDebugEnabled()) |
| log.debug(sm.getString("jaasRealm.beginLogin", username, appName)); |
| |
| // What if the LoginModule is in the container class loader ? |
| ClassLoader ocl = null; |
| |
| if (!isUseContextClassLoader()) { |
| ocl = Thread.currentThread().getContextClassLoader(); |
| Thread.currentThread().setContextClassLoader( |
| this.getClass().getClassLoader()); |
| } |
| |
| try { |
| Configuration config = getConfig(); |
| loginContext = new LoginContext( |
| appName, null, callbackHandler, config); |
| } catch (Throwable e) { |
| ExceptionUtils.handleThrowable(e); |
| log.error(sm.getString("jaasRealm.unexpectedError"), e); |
| return (null); |
| } finally { |
| if(!isUseContextClassLoader()) { |
| Thread.currentThread().setContextClassLoader(ocl); |
| } |
| } |
| |
| if( log.isDebugEnabled()) |
| log.debug("Login context created " + username); |
| |
| // Negotiate a login via this LoginContext |
| Subject subject = null; |
| try { |
| loginContext.login(); |
| subject = loginContext.getSubject(); |
| if (subject == null) { |
| if( log.isDebugEnabled()) |
| log.debug(sm.getString("jaasRealm.failedLogin", username)); |
| return (null); |
| } |
| } catch (AccountExpiredException e) { |
| if (log.isDebugEnabled()) |
| log.debug(sm.getString("jaasRealm.accountExpired", username)); |
| return (null); |
| } catch (CredentialExpiredException e) { |
| if (log.isDebugEnabled()) |
| log.debug(sm.getString("jaasRealm.credentialExpired", username)); |
| return (null); |
| } catch (FailedLoginException e) { |
| if (log.isDebugEnabled()) |
| log.debug(sm.getString("jaasRealm.failedLogin", username)); |
| return (null); |
| } catch (LoginException e) { |
| log.warn(sm.getString("jaasRealm.loginException", username), e); |
| return (null); |
| } catch (Throwable e) { |
| ExceptionUtils.handleThrowable(e); |
| log.error(sm.getString("jaasRealm.unexpectedError"), e); |
| return (null); |
| } |
| |
| if( log.isDebugEnabled()) |
| log.debug(sm.getString("jaasRealm.loginContextCreated", username)); |
| |
| // Return the appropriate Principal for this authenticated Subject |
| Principal principal = createPrincipal(username, subject, loginContext); |
| if (principal == null) { |
| log.debug(sm.getString("jaasRealm.authenticateFailure", username)); |
| return (null); |
| } |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("jaasRealm.authenticateSuccess", username)); |
| } |
| |
| return (principal); |
| } catch( Throwable t) { |
| log.error( "error ", t); |
| return null; |
| } |
| } |
| |
| /** |
| * Return a short name for this <code>Realm</code> implementation. |
| */ |
| @Override |
| protected String getName() { |
| |
| return (name); |
| |
| } |
| |
| |
| /** |
| * Return the password associated with the given principal's user name. This |
| * always returns null as the JAASRealm has no way of obtaining this |
| * information. |
| */ |
| @Override |
| protected String getPassword(String username) { |
| |
| return (null); |
| |
| } |
| |
| |
| /** |
| * Return the <code>Principal</code> associated with the given user name. |
| */ |
| @Override |
| protected Principal getPrincipal(String username) { |
| |
| return authenticate(username, |
| new JAASCallbackHandler(this, username, null, null, null, null, |
| null, null, null, HttpServletRequest.CLIENT_CERT_AUTH)); |
| |
| } |
| |
| |
| /** |
| * Identify and return a <code>java.security.Principal</code> instance |
| * representing the authenticated user for the specified <code>Subject</code>. |
| * The Principal is constructed by scanning the list of Principals returned |
| * by the JAASLoginModule. The first <code>Principal</code> object that matches |
| * one of the class names supplied as a "user class" is the user Principal. |
| * This object is returned to the caller. |
| * Any remaining principal objects returned by the LoginModules are mapped to |
| * roles, but only if their respective classes match one of the "role class" classes. |
| * If a user Principal cannot be constructed, return <code>null</code>. |
| * @param subject The <code>Subject</code> representing the logged-in user |
| * @param loginContext Associated with the Principal so |
| * {@link LoginContext#logout()} can be called later |
| */ |
| protected Principal createPrincipal(String username, Subject subject, |
| LoginContext loginContext) { |
| // Prepare to scan the Principals for this Subject |
| |
| List<String> roles = new ArrayList<>(); |
| Principal userPrincipal = null; |
| |
| // Scan the Principals for this Subject |
| Iterator<Principal> principals = subject.getPrincipals().iterator(); |
| while (principals.hasNext()) { |
| Principal principal = principals.next(); |
| |
| String principalClass = principal.getClass().getName(); |
| |
| if( log.isDebugEnabled() ) { |
| log.debug(sm.getString("jaasRealm.checkPrincipal", principal, principalClass)); |
| } |
| |
| if (userPrincipal == null && userClasses.contains(principalClass)) { |
| userPrincipal = principal; |
| if( log.isDebugEnabled() ) { |
| log.debug(sm.getString("jaasRealm.userPrincipalSuccess", principal.getName())); |
| } |
| } |
| |
| if (roleClasses.contains(principalClass)) { |
| roles.add(principal.getName()); |
| if( log.isDebugEnabled() ) { |
| log.debug(sm.getString("jaasRealm.rolePrincipalAdd", principal.getName())); |
| } |
| } |
| } |
| |
| // Print failure message if needed |
| if (userPrincipal == null) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("jaasRealm.userPrincipalFailure")); |
| log.debug(sm.getString("jaasRealm.rolePrincipalFailure")); |
| } |
| } else { |
| if (roles.size() == 0) { |
| if (log.isDebugEnabled()) { |
| log.debug(sm.getString("jaasRealm.rolePrincipalFailure")); |
| } |
| } |
| } |
| |
| // Return the resulting Principal for our authenticated user |
| return new GenericPrincipal(username, null, roles, userPrincipal, |
| loginContext); |
| } |
| |
| /** |
| * Ensure the given name is legal for JAAS configuration. |
| * Added for Bugzilla 30869, made protected for easy customization |
| * in case my implementation is insufficient, which I think is |
| * very likely. |
| * |
| * @param src The name to validate |
| * @return A string that's a valid JAAS realm name |
| */ |
| protected String makeLegalForJAAS(final String src) { |
| String result = src; |
| |
| // Default name is "other" per JAAS spec |
| if(result == null) { |
| result = "other"; |
| } |
| |
| // Strip leading slash if present, as Sun JAAS impl |
| // barfs on it (see Bugzilla 30869 bug report). |
| if(result.startsWith("/")) { |
| result = result.substring(1); |
| } |
| |
| return result; |
| } |
| |
| |
| // ------------------------------------------------------ Lifecycle Methods |
| |
| |
| /** |
| * Prepare for the beginning of active use of the public methods of this |
| * component and implement the requirements of |
| * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. |
| * |
| * @exception LifecycleException if this component detects a fatal error |
| * that prevents this component from being used |
| */ |
| @Override |
| protected void startInternal() throws LifecycleException { |
| |
| // These need to be called after loading configuration, in case |
| // useContextClassLoader appears after them in xml config |
| parseClassNames(userClassNames, userClasses); |
| parseClassNames(roleClassNames, roleClasses); |
| |
| super.startInternal(); |
| } |
| |
| |
| /** |
| * Load custom JAAS Configuration |
| */ |
| protected Configuration getConfig() { |
| try { |
| if (jaasConfigurationLoaded) { |
| return jaasConfiguration; |
| } |
| synchronized (this) { |
| if (configFile == null) { |
| jaasConfigurationLoaded = true; |
| return null; |
| } |
| URL resource = Thread.currentThread().getContextClassLoader(). |
| getResource(configFile); |
| URI uri = resource.toURI(); |
| @SuppressWarnings("unchecked") |
| Class<Configuration> sunConfigFile = (Class<Configuration>) |
| Class.forName("com.sun.security.auth.login.ConfigFile"); |
| Constructor<Configuration> constructor = |
| sunConfigFile.getConstructor(URI.class); |
| Configuration config = constructor.newInstance(uri); |
| this.jaasConfiguration = config; |
| this.jaasConfigurationLoaded = true; |
| return this.jaasConfiguration; |
| } |
| } catch (URISyntaxException ex) { |
| throw new RuntimeException(ex); |
| } catch (NoSuchMethodException ex) { |
| throw new RuntimeException(ex); |
| } catch (SecurityException ex) { |
| throw new RuntimeException(ex); |
| } catch (InstantiationException ex) { |
| throw new RuntimeException(ex); |
| } catch (IllegalAccessException ex) { |
| throw new RuntimeException(ex); |
| } catch (IllegalArgumentException ex) { |
| throw new RuntimeException(ex); |
| } catch (InvocationTargetException ex) { |
| throw new RuntimeException(ex.getCause()); |
| } catch (ClassNotFoundException ex) { |
| throw new RuntimeException(ex); |
| } |
| |
| } |
| } |