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