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 }