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 }