1 /*
   2  * Copyright (c) 2004, 2008, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.jmx.remote.security;
  27 
  28 import java.io.IOException;
  29 import java.security.AccessController;
  30 import java.security.Principal;
  31 import java.security.PrivilegedAction;
  32 import java.security.PrivilegedActionException;
  33 import java.security.PrivilegedExceptionAction;
  34 import java.util.Collections;
  35 import java.util.HashMap;
  36 import java.util.Map;
  37 import java.util.Properties;
  38 import javax.management.remote.JMXPrincipal;
  39 import javax.management.remote.JMXAuthenticator;
  40 import javax.security.auth.AuthPermission;
  41 import javax.security.auth.Subject;
  42 import javax.security.auth.callback.*;
  43 import javax.security.auth.login.AppConfigurationEntry;
  44 import javax.security.auth.login.Configuration;
  45 import javax.security.auth.login.LoginContext;
  46 import javax.security.auth.login.LoginException;
  47 import javax.security.auth.spi.LoginModule;
  48 import com.sun.jmx.remote.util.ClassLogger;
  49 import com.sun.jmx.remote.util.EnvHelp;
  50 
  51 /**
  52  * <p>This class represents a
  53  * <a href="{@docRoot}/../guide/security/jaas/JAASRefGuide.html">JAAS</a>
  54  * based implementation of the {@link JMXAuthenticator} interface.</p>
  55  *
  56  * <p>Authentication is performed by passing the supplied user's credentials
  57  * to one or more authentication mechanisms ({@link LoginModule}) for
  58  * verification. An authentication mechanism acquires the user's credentials
  59  * by calling {@link NameCallback} and/or {@link PasswordCallback}.
  60  * If authentication is successful then an authenticated {@link Subject}
  61  * filled in with a {@link Principal} is returned.  Authorization checks
  62  * will then be performed based on this <code>Subject</code>.</p>
  63  *
  64  * <p>By default, a single file-based authentication mechanism
  65  * {@link FileLoginModule} is configured (<code>FileLoginConfig</code>).</p>
  66  *
  67  * <p>To override the default configuration use the
  68  * <code>com.sun.management.jmxremote.login.config</code> management property
  69  * described in the JRE/conf/management/management.properties file.
  70  * Set this property to the name of a JAAS configuration entry and ensure that
  71  * the entry is loaded by the installed {@link Configuration}. In addition,
  72  * ensure that the authentication mechanisms specified in the entry acquire
  73  * the user's credentials by calling {@link NameCallback} and
  74  * {@link PasswordCallback} and that they return a {@link Subject} filled-in
  75  * with a {@link Principal}, for those users that are successfully
  76  * authenticated.</p>
  77  */
  78 public final class JMXPluggableAuthenticator implements JMXAuthenticator {
  79 
  80     /**
  81      * Creates an instance of <code>JMXPluggableAuthenticator</code>
  82      * and initializes it with a {@link LoginContext}.
  83      *
  84      * @param env the environment containing configuration properties for the
  85      *            authenticator. Can be null, which is equivalent to an empty
  86      *            Map.
  87      * @exception SecurityException if the authentication mechanism cannot be
  88      *            initialized.
  89      */
  90     public JMXPluggableAuthenticator(Map<?, ?> env) {
  91 
  92         String loginConfigName = null;
  93         String passwordFile = null;
  94         String hashPasswords = null;
  95 
  96         if (env != null) {
  97             loginConfigName = (String) env.get(LOGIN_CONFIG_PROP);
  98             passwordFile = (String) env.get(PASSWORD_FILE_PROP);
  99             hashPasswords = (String) env.get(HASH_PASSWORDS);
 100         }
 101 
 102         try {
 103 
 104             if (loginConfigName != null) {
 105                 // use the supplied JAAS login configuration
 106                 loginContext =
 107                     new LoginContext(loginConfigName, new JMXCallbackHandler());
 108 
 109             } else {
 110                 // use the default JAAS login configuration (file-based)
 111                 SecurityManager sm = System.getSecurityManager();
 112                 if (sm != null) {
 113                     sm.checkPermission(
 114                             new AuthPermission("createLoginContext." +
 115                                                LOGIN_CONFIG_NAME));
 116                 }
 117 
 118                 final String pf = passwordFile;
 119                 final String hashPass = hashPasswords;
 120                 try {
 121                     loginContext = AccessController.doPrivileged(
 122                         new PrivilegedExceptionAction<LoginContext>() {
 123                             public LoginContext run() throws LoginException {
 124                                 return new LoginContext(
 125                                                 LOGIN_CONFIG_NAME,
 126                                                 null,
 127                                                 new JMXCallbackHandler(),
 128                                                 new FileLoginConfig(pf,hashPass));
 129                             }
 130                         });
 131                 } catch (PrivilegedActionException pae) {
 132                     throw (LoginException) pae.getException();
 133                 }
 134             }
 135 
 136         } catch (LoginException le) {
 137             authenticationFailure("authenticate", le);
 138 
 139         } catch (SecurityException se) {
 140             authenticationFailure("authenticate", se);
 141         }
 142     }
 143 
 144     /**
 145      * Authenticate the <code>MBeanServerConnection</code> client
 146      * with the given client credentials.
 147      *
 148      * @param credentials the user-defined credentials to be passed in
 149      * to the server in order to authenticate the user before creating
 150      * the <code>MBeanServerConnection</code>.  This parameter must
 151      * be a two-element <code>String[]</code> containing the client's
 152      * username and password in that order.
 153      *
 154      * @return the authenticated subject containing a
 155      * <code>JMXPrincipal(username)</code>.
 156      *
 157      * @exception SecurityException if the server cannot authenticate the user
 158      * with the provided credentials.
 159      */
 160     public Subject authenticate(Object credentials) {
 161         // Verify that credentials is of type String[].
 162         //
 163         if (!(credentials instanceof String[])) {
 164             // Special case for null so we get a more informative message
 165             if (credentials == null)
 166                 authenticationFailure("authenticate", "Credentials required");
 167 
 168             final String message =
 169                 "Credentials should be String[] instead of " +
 170                  credentials.getClass().getName();
 171             authenticationFailure("authenticate", message);
 172         }
 173         // Verify that the array contains two elements.
 174         //
 175         final String[] aCredentials = (String[]) credentials;
 176         if (aCredentials.length != 2) {
 177             final String message =
 178                 "Credentials should have 2 elements not " +
 179                 aCredentials.length;
 180             authenticationFailure("authenticate", message);
 181         }
 182         // Verify that username exists and the associated
 183         // password matches the one supplied by the client.
 184         //
 185         username = aCredentials[0];
 186         password = aCredentials[1];
 187         if (username == null || password == null) {
 188             final String message = "Username or password is null";
 189             authenticationFailure("authenticate", message);
 190         }
 191 
 192         // Perform authentication
 193         try {
 194             loginContext.login();
 195             final Subject subject = loginContext.getSubject();
 196             AccessController.doPrivileged(new PrivilegedAction<Void>() {
 197                     public Void run() {
 198                         subject.setReadOnly();
 199                         return null;
 200                     }
 201                 });
 202 
 203             return subject;
 204 
 205         } catch (LoginException le) {
 206             authenticationFailure("authenticate", le);
 207         }
 208         return null;
 209     }
 210 
 211     private static void authenticationFailure(String method, String message)
 212         throws SecurityException {
 213         final String msg = "Authentication failed! " + message;
 214         final SecurityException e = new SecurityException(msg);
 215         logException(method, msg, e);
 216         throw e;
 217     }
 218 
 219     private static void authenticationFailure(String method,
 220                                               Exception exception)
 221         throws SecurityException {
 222         String msg;
 223         SecurityException se;
 224         if (exception instanceof SecurityException) {
 225             msg = exception.getMessage();
 226             se = (SecurityException) exception;
 227         } else {
 228             msg = "Authentication failed! " + exception.getMessage();
 229             final SecurityException e = new SecurityException(msg);
 230             EnvHelp.initCause(e, exception);
 231             se = e;
 232         }
 233         logException(method, msg, se);
 234         throw se;
 235     }
 236 
 237     private static void logException(String method,
 238                                      String message,
 239                                      Exception e) {
 240         if (logger.traceOn()) {
 241             logger.trace(method, message);
 242         }
 243         if (logger.debugOn()) {
 244             logger.debug(method, e);
 245         }
 246     }
 247 
 248     private LoginContext loginContext;
 249     private String username;
 250     private String password;
 251     private static final String LOGIN_CONFIG_PROP =
 252         "jmx.remote.x.login.config";
 253     private static final String LOGIN_CONFIG_NAME = "JMXPluggableAuthenticator";
 254     private static final String PASSWORD_FILE_PROP =
 255         "jmx.remote.x.password.file";
 256     private static final String HASH_PASSWORDS =
 257         "jmx.remote.x.password.file.hash";
 258     private static final ClassLogger logger =
 259         new ClassLogger("javax.management.remote.misc", LOGIN_CONFIG_NAME);
 260 
 261 /**
 262  * This callback handler supplies the username and password (which was
 263  * originally supplied by the JMX user) to the JAAS login module performing
 264  * the authentication. No interactive user prompting is required because the
 265  * credentials are already available to this class (via its enclosing class).
 266  */
 267 private final class JMXCallbackHandler implements CallbackHandler {
 268 
 269     /**
 270      * Sets the username and password in the appropriate Callback object.
 271      */
 272     public void handle(Callback[] callbacks)
 273         throws IOException, UnsupportedCallbackException {
 274 
 275         for (int i = 0; i < callbacks.length; i++) {
 276             if (callbacks[i] instanceof NameCallback) {
 277                 ((NameCallback)callbacks[i]).setName(username);
 278 
 279             } else if (callbacks[i] instanceof PasswordCallback) {
 280                 ((PasswordCallback)callbacks[i])
 281                     .setPassword(password.toCharArray());
 282 
 283             } else {
 284                 throw new UnsupportedCallbackException
 285                     (callbacks[i], "Unrecognized Callback");
 286             }
 287         }
 288     }
 289 }
 290 
 291 /**
 292  * This class defines the JAAS configuration for file-based authentication.
 293  * It is equivalent to the following textual configuration entry:
 294  * <pre>
 295  *     JMXPluggableAuthenticator {
 296  *         com.sun.jmx.remote.security.FileLoginModule required;
 297  *     };
 298  * </pre>
 299  */
 300 private static class FileLoginConfig extends Configuration {
 301 
 302     // The JAAS configuration for file-based authentication
 303     private AppConfigurationEntry[] entries;
 304 
 305     // The classname of the login module for file-based authentication
 306     private static final String FILE_LOGIN_MODULE =
 307         FileLoginModule.class.getName();
 308 
 309     // The option that identifies the password file to use
 310     private static final String PASSWORD_FILE_OPTION = "passwordFile";
 311     private static final String HASH_PASSWORDS = "hashPassword";
 312 
 313     /**
 314      * Creates an instance of <code>FileLoginConfig</code>
 315      *
 316      * @param passwordFile A filepath that identifies the password file to use.
 317      *                     If null then the default password file is used.
 318      */
 319     public FileLoginConfig(String passwordFile, String hashPasswords) {
 320 
 321         Map<String, String> options;
 322         if (passwordFile != null) {
 323             options = new HashMap<String, String>(1);
 324             options.put(PASSWORD_FILE_OPTION, passwordFile);
 325             options.put(HASH_PASSWORDS, hashPasswords);
 326         } else {
 327             options = Collections.emptyMap();
 328         }
 329 
 330         entries = new AppConfigurationEntry[] {
 331             new AppConfigurationEntry(FILE_LOGIN_MODULE,
 332                 AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
 333                     options)
 334         };
 335     }
 336 
 337     /**
 338      * Gets the JAAS configuration for file-based authentication
 339      */
 340     public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
 341 
 342         return name.equals(LOGIN_CONFIG_NAME) ? entries : null;
 343     }
 344 
 345     /**
 346      * Refreshes the configuration.
 347      */
 348     public void refresh() {
 349         // the configuration is fixed
 350     }
 351 }
 352 
 353 }