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 
  95         if (env != null) {
  96             loginConfigName = (String) env.get(LOGIN_CONFIG_PROP);
  97             passwordFile = (String) env.get(PASSWORD_FILE_PROP);
  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                 try {
 118                     loginContext = AccessController.doPrivileged(
 119                         new PrivilegedExceptionAction<LoginContext>() {
 120                             public LoginContext run() throws LoginException {
 121                                 return new LoginContext(
 122                                                 LOGIN_CONFIG_NAME,
 123                                                 null,
 124                                                 new JMXCallbackHandler(),
 125                                                 new FileLoginConfig(pf));
 126                             }
 127                         });
 128                 } catch (PrivilegedActionException pae) {
 129                     throw (LoginException) pae.getException();
 130                 }
 131             }
 132 
 133         } catch (LoginException le) {
 134             authenticationFailure("authenticate", le);
 135 
 136         } catch (SecurityException se) {
 137             authenticationFailure("authenticate", se);
 138         }
 139     }
 140 
 141     /**
 142      * Authenticate the <code>MBeanServerConnection</code> client
 143      * with the given client credentials.
 144      *
 145      * @param credentials the user-defined credentials to be passed in
 146      * to the server in order to authenticate the user before creating
 147      * the <code>MBeanServerConnection</code>.  This parameter must
 148      * be a two-element <code>String[]</code> containing the client's
 149      * username and password in that order.
 150      *
 151      * @return the authenticated subject containing a
 152      * <code>JMXPrincipal(username)</code>.
 153      *
 154      * @exception SecurityException if the server cannot authenticate the user
 155      * with the provided credentials.
 156      */
 157     public Subject authenticate(Object credentials) {
 158         // Verify that credentials is of type String[].
 159         //
 160         if (!(credentials instanceof String[])) {
 161             // Special case for null so we get a more informative message
 162             if (credentials == null)
 163                 authenticationFailure("authenticate", "Credentials required");
 164 
 165             final String message =
 166                 "Credentials should be String[] instead of " +
 167                  credentials.getClass().getName();
 168             authenticationFailure("authenticate", message);
 169         }
 170         // Verify that the array contains two elements.
 171         //
 172         final String[] aCredentials = (String[]) credentials;
 173         if (aCredentials.length != 2) {
 174             final String message =
 175                 "Credentials should have 2 elements not " +
 176                 aCredentials.length;
 177             authenticationFailure("authenticate", message);
 178         }
 179         // Verify that username exists and the associated
 180         // password matches the one supplied by the client.
 181         //
 182         username = aCredentials[0];
 183         password = aCredentials[1];
 184         if (username == null || password == null) {
 185             final String message = "Username or password is null";
 186             authenticationFailure("authenticate", message);
 187         }
 188 
 189         // Perform authentication
 190         try {
 191             loginContext.login();
 192             final Subject subject = loginContext.getSubject();
 193             AccessController.doPrivileged(new PrivilegedAction<Void>() {
 194                     public Void run() {
 195                         subject.setReadOnly();
 196                         return null;
 197                     }
 198                 });
 199 
 200             return subject;
 201 
 202         } catch (LoginException le) {
 203             authenticationFailure("authenticate", le);
 204         }
 205         return null;
 206     }
 207 
 208     private static void authenticationFailure(String method, String message)
 209         throws SecurityException {
 210         final String msg = "Authentication failed! " + message;
 211         final SecurityException e = new SecurityException(msg);
 212         logException(method, msg, e);
 213         throw e;
 214     }
 215 
 216     private static void authenticationFailure(String method,
 217                                               Exception exception)
 218         throws SecurityException {
 219         String msg;
 220         SecurityException se;
 221         if (exception instanceof SecurityException) {
 222             msg = exception.getMessage();
 223             se = (SecurityException) exception;
 224         } else {
 225             msg = "Authentication failed! " + exception.getMessage();
 226             final SecurityException e = new SecurityException(msg);
 227             EnvHelp.initCause(e, exception);
 228             se = e;
 229         }
 230         logException(method, msg, se);
 231         throw se;
 232     }
 233 
 234     private static void logException(String method,
 235                                      String message,
 236                                      Exception e) {
 237         if (logger.traceOn()) {
 238             logger.trace(method, message);
 239         }
 240         if (logger.debugOn()) {
 241             logger.debug(method, e);
 242         }
 243     }
 244 
 245     private LoginContext loginContext;
 246     private String username;
 247     private String password;
 248     private static final String LOGIN_CONFIG_PROP =
 249         "jmx.remote.x.login.config";
 250     private static final String LOGIN_CONFIG_NAME = "JMXPluggableAuthenticator";
 251     private static final String PASSWORD_FILE_PROP =
 252         "jmx.remote.x.password.file";
 253     private static final ClassLogger logger =
 254         new ClassLogger("javax.management.remote.misc", LOGIN_CONFIG_NAME);
 255 
 256 /**
 257  * This callback handler supplies the username and password (which was
 258  * originally supplied by the JMX user) to the JAAS login module performing
 259  * the authentication. No interactive user prompting is required because the
 260  * credentials are already available to this class (via its enclosing class).
 261  */
 262 private final class JMXCallbackHandler implements CallbackHandler {
 263 
 264     /**
 265      * Sets the username and password in the appropriate Callback object.
 266      */
 267     public void handle(Callback[] callbacks)
 268         throws IOException, UnsupportedCallbackException {
 269 
 270         for (int i = 0; i < callbacks.length; i++) {
 271             if (callbacks[i] instanceof NameCallback) {
 272                 ((NameCallback)callbacks[i]).setName(username);
 273 
 274             } else if (callbacks[i] instanceof PasswordCallback) {
 275                 ((PasswordCallback)callbacks[i])
 276                     .setPassword(password.toCharArray());
 277 
 278             } else {
 279                 throw new UnsupportedCallbackException
 280                     (callbacks[i], "Unrecognized Callback");
 281             }
 282         }
 283     }
 284 }
 285 
 286 /**
 287  * This class defines the JAAS configuration for file-based authentication.
 288  * It is equivalent to the following textual configuration entry:
 289  * <pre>
 290  *     JMXPluggableAuthenticator {
 291  *         com.sun.jmx.remote.security.FileLoginModule required;
 292  *     };
 293  * </pre>
 294  */
 295 private static class FileLoginConfig extends Configuration {
 296 
 297     // The JAAS configuration for file-based authentication
 298     private AppConfigurationEntry[] entries;
 299 
 300     // The classname of the login module for file-based authentication
 301     private static final String FILE_LOGIN_MODULE =
 302         FileLoginModule.class.getName();
 303 
 304     // The option that identifies the password file to use
 305     private static final String PASSWORD_FILE_OPTION = "passwordFile";
 306 
 307     /**
 308      * Creates an instance of <code>FileLoginConfig</code>
 309      *
 310      * @param passwordFile A filepath that identifies the password file to use.
 311      *                     If null then the default password file is used.
 312      */
 313     public FileLoginConfig(String passwordFile) {
 314 
 315         Map<String, String> options;
 316         if (passwordFile != null) {
 317             options = new HashMap<String, String>(1);
 318             options.put(PASSWORD_FILE_OPTION, passwordFile);
 319         } else {
 320             options = Collections.emptyMap();
 321         }
 322 
 323         entries = new AppConfigurationEntry[] {
 324             new AppConfigurationEntry(FILE_LOGIN_MODULE,
 325                 AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
 326                     options)
 327         };
 328     }
 329 
 330     /**
 331      * Gets the JAAS configuration for file-based authentication
 332      */
 333     public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
 334 
 335         return name.equals(LOGIN_CONFIG_NAME) ? entries : null;
 336     }
 337 
 338     /**
 339      * Refreshes the configuration.
 340      */
 341     public void refresh() {
 342         // the configuration is fixed
 343     }
 344 }
 345 
 346 }