1. /*
  2. * @(#)FileLoginModule.java 1.3 04/05/05
  3. *
  4. * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package com.sun.jmx.remote.security;
  8. import java.io.BufferedInputStream;
  9. import java.io.File;
  10. import java.io.FileInputStream;
  11. import java.io.IOException;
  12. import java.security.AccessController;
  13. import java.util.Arrays;
  14. import java.util.Hashtable;
  15. import java.util.Map;
  16. import java.util.Properties;
  17. import javax.security.auth.*;
  18. import javax.security.auth.callback.*;
  19. import javax.security.auth.login.*;
  20. import javax.security.auth.spi.*;
  21. import javax.management.remote.JMXPrincipal;
  22. import com.sun.jmx.remote.util.ClassLogger;
  23. import com.sun.jmx.remote.util.EnvHelp;
  24. import sun.management.jmxremote.ConnectorBootstrap;
  25. import sun.security.action.GetPropertyAction;
  26. /**
  27. * This {@link LoginModule} performs file-based authentication.
  28. *
  29. * <p> A supplied username and password is verified against the
  30. * corresponding user credentials stored in a designated password file.
  31. * If successful then a new {@link JMXPrincipal} is created with the
  32. * user's name and it is associated with the current {@link Subject}.
  33. * Such principals may be identified and granted management privileges in
  34. * the access control file for JMX remote management or in a Java security
  35. * policy.
  36. *
  37. * <p> The password file comprises a list of key-value pairs as specified in
  38. * {@link Properties}. The key represents a user's name and the value is its
  39. * associated cleartext password. By default, the following password file is
  40. * used:
  41. * <pre>
  42. * ${java.home}/lib/management/jmxremote.password
  43. * </pre>
  44. * A different password file can be specified via the <code>passwordFile</code>
  45. * configuration option.
  46. *
  47. * <p> This module recognizes the following <code>Configuration</code> options:
  48. * <dl>
  49. * <dt> <code>passwordFile</code> </dt>
  50. * <dd> the path to an alternative password file. It is used instead of
  51. * the default password file.</dd>
  52. *
  53. * <dt> <code>useFirstPass</code> </dt>
  54. * <dd> if <code>true</code>, this module retrieves the username and password
  55. * from the module's shared state, using "javax.security.auth.login.name"
  56. * and "javax.security.auth.login.password" as the respective keys. The
  57. * retrieved values are used for authentication. If authentication fails,
  58. * no attempt for a retry is made, and the failure is reported back to
  59. * the calling application.</dd>
  60. *
  61. * <dt> <code>tryFirstPass</code> </dt>
  62. * <dd> if <code>true</code>, this module retrieves the username and password
  63. * from the module's shared state, using "javax.security.auth.login.name"
  64. * and "javax.security.auth.login.password" as the respective keys. The
  65. * retrieved values are used for authentication. If authentication fails,
  66. * the module uses the CallbackHandler to retrieve a new username and
  67. * password, and another attempt to authenticate is made. If the
  68. * authentication fails, the failure is reported back to the calling
  69. * application.</dd>
  70. *
  71. * <dt> <code>storePass</code> </dt>
  72. * <dd> if <code>true</code>, this module stores the username and password
  73. * obtained from the CallbackHandler in the module's shared state, using
  74. * "javax.security.auth.login.name" and
  75. * "javax.security.auth.login.password" as the respective keys. This is
  76. * not performed if existing values already exist for the username and
  77. * password in the shared state, or if authentication fails.</dd>
  78. *
  79. * <dt> <code>clearPass</code> </dt>
  80. * <dd> if <code>true</code>, this module clears the username and password
  81. * stored in the module's shared state after both phases of authentication
  82. * (login and commit) have completed.</dd>
  83. * </dl>
  84. */
  85. public class FileLoginModule implements LoginModule {
  86. // Location of the default password file
  87. private static final String DEFAULT_PASSWORD_FILE_NAME =
  88. ((String) AccessController.doPrivileged(
  89. new GetPropertyAction("java.home"))) +
  90. File.separatorChar + "lib" +
  91. File.separatorChar + "management" + File.separatorChar +
  92. ConnectorBootstrap.DefaultValues.PASSWORD_FILE_NAME;
  93. // Key to retrieve the stored username
  94. private static final String USERNAME_KEY =
  95. "javax.security.auth.login.name";
  96. // Key to retrieve the stored password
  97. private static final String PASSWORD_KEY =
  98. "javax.security.auth.login.password";
  99. // Log messages
  100. private static final ClassLogger logger =
  101. new ClassLogger("javax.management.remote.misc", "FileLoginModule");
  102. // Configurable options
  103. private boolean useFirstPass = false;
  104. private boolean tryFirstPass = false;
  105. private boolean storePass = false;
  106. private boolean clearPass = false;
  107. // Authentication status
  108. private boolean succeeded = false;
  109. private boolean commitSucceeded = false;
  110. // Supplied username and password
  111. private String username;
  112. private char[] password;
  113. private JMXPrincipal user;
  114. // Initial state
  115. private Subject subject;
  116. private CallbackHandler callbackHandler;
  117. private Map sharedState;
  118. private Map options;
  119. private String passwordFile;
  120. private Properties userCredentials;
  121. /**
  122. * Initialize this <code>LoginModule</code>.
  123. *
  124. * @param subject the <code>Subject</code> to be authenticated.
  125. * @param callbackHandler a <code>CallbackHandler</code> to acquire the
  126. * user's name and password.
  127. * @param sharedState shared <code>LoginModule</code> state.
  128. * @param options options specified in the login
  129. * <code>Configuration</code> for this particular
  130. * <code>LoginModule</code>.
  131. */
  132. public void initialize(Subject subject, CallbackHandler callbackHandler,
  133. Map<String,?> sharedState,
  134. Map<String,?> options)
  135. {
  136. this.subject = subject;
  137. this.callbackHandler = callbackHandler;
  138. this.sharedState = sharedState;
  139. this.options = options;
  140. // initialize any configured options
  141. tryFirstPass =
  142. "true".equalsIgnoreCase((String)options.get("tryFirstPass"));
  143. useFirstPass =
  144. "true".equalsIgnoreCase((String)options.get("useFirstPass"));
  145. storePass =
  146. "true".equalsIgnoreCase((String)options.get("storePass"));
  147. clearPass =
  148. "true".equalsIgnoreCase((String)options.get("clearPass"));
  149. passwordFile = (String)options.get("passwordFile");
  150. // set the location of the password file
  151. if (passwordFile == null) {
  152. passwordFile = DEFAULT_PASSWORD_FILE_NAME;
  153. }
  154. }
  155. /**
  156. * Begin user authentication (Authentication Phase 1).
  157. *
  158. * <p> Acquire the user's name and password and verify them against
  159. * the corresponding credentials from the password file.
  160. *
  161. * @return true always, since this <code>LoginModule</code>
  162. * should not be ignored.
  163. * @exception FailedLoginException if the authentication fails.
  164. * @exception LoginException if this <code>LoginModule</code>
  165. * is unable to perform the authentication.
  166. */
  167. public boolean login() throws LoginException {
  168. try {
  169. loadPasswordFile();
  170. } catch (IOException ioe) {
  171. LoginException le = new LoginException
  172. ("Error: unable to load the password file: " + passwordFile);
  173. throw (LoginException) EnvHelp.initCause(le, ioe);
  174. }
  175. if (userCredentials == null) {
  176. throw new LoginException
  177. ("Error: unable to locate the users' credentials.");
  178. }
  179. if (logger.debugOn()) {
  180. logger.debug("login", "Using password file: " + passwordFile);
  181. }
  182. // attempt the authentication
  183. if (tryFirstPass) {
  184. try {
  185. // attempt the authentication by getting the
  186. // username and password from shared state
  187. attemptAuthentication(true);
  188. // authentication succeeded
  189. succeeded = true;
  190. if (logger.debugOn()) {
  191. logger.debug("login",
  192. "Authentication using cached password has succeeded");
  193. }
  194. return true;
  195. } catch (LoginException le) {
  196. // authentication failed -- try again below by prompting
  197. cleanState();
  198. logger.debug("login",
  199. "Authentication using cached password has failed");
  200. }
  201. } else if (useFirstPass) {
  202. try {
  203. // attempt the authentication by getting the
  204. // username and password from shared state
  205. attemptAuthentication(true);
  206. // authentication succeeded
  207. succeeded = true;
  208. if (logger.debugOn()) {
  209. logger.debug("login",
  210. "Authentication using cached password has succeeded");
  211. }
  212. return true;
  213. } catch (LoginException le) {
  214. // authentication failed
  215. cleanState();
  216. logger.debug("login",
  217. "Authentication using cached password has failed");
  218. throw le;
  219. }
  220. }
  221. if (logger.debugOn()) {
  222. logger.debug("login", "Acquiring password");
  223. }
  224. // attempt the authentication using the supplied username and password
  225. try {
  226. attemptAuthentication(false);
  227. // authentication succeeded
  228. succeeded = true;
  229. if (logger.debugOn()) {
  230. logger.debug("login", "Authentication has succeeded");
  231. }
  232. return true;
  233. } catch (LoginException le) {
  234. cleanState();
  235. logger.debug("login", "Authentication has failed");
  236. throw le;
  237. }
  238. }
  239. /**
  240. * Complete user authentication (Authentication Phase 2).
  241. *
  242. * <p> This method is called if the LoginContext's
  243. * overall authentication has succeeded
  244. * (all the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL
  245. * LoginModules have succeeded).
  246. *
  247. * <p> If this LoginModule's own authentication attempt
  248. * succeeded (checked by retrieving the private state saved by the
  249. * <code>login</code> method), then this method associates a
  250. * <code>JMXPrincipal</code> with the <code>Subject</code> located in the
  251. * <code>LoginModule</code>. If this LoginModule's own
  252. * authentication attempted failed, then this method removes
  253. * any state that was originally saved.
  254. *
  255. * @exception LoginException if the commit fails
  256. * @return true if this LoginModule's own login and commit
  257. * attempts succeeded, or false otherwise.
  258. */
  259. public boolean commit() throws LoginException {
  260. if (succeeded == false) {
  261. return false;
  262. } else {
  263. if (subject.isReadOnly()) {
  264. cleanState();
  265. throw new LoginException("Subject is read-only");
  266. }
  267. // add Principals to the Subject
  268. if (!subject.getPrincipals().contains(user)) {
  269. subject.getPrincipals().add(user);
  270. }
  271. if (logger.debugOn()) {
  272. logger.debug("commit",
  273. "Authentication has completed successfully");
  274. }
  275. }
  276. // in any case, clean out state
  277. cleanState();
  278. commitSucceeded = true;
  279. return true;
  280. }
  281. /**
  282. * Abort user authentication (Authentication Phase 2).
  283. *
  284. * <p> This method is called if the LoginContext's overall authentication
  285. * failed (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL
  286. * LoginModules did not succeed).
  287. *
  288. * <p> If this LoginModule's own authentication attempt
  289. * succeeded (checked by retrieving the private state saved by the
  290. * <code>login</code> and <code>commit</code> methods),
  291. * then this method cleans up any state that was originally saved.
  292. *
  293. * @exception LoginException if the abort fails.
  294. * @return false if this LoginModule's own login and/or commit attempts
  295. * failed, and true otherwise.
  296. */
  297. public boolean abort() throws LoginException {
  298. if (logger.debugOn()) {
  299. logger.debug("abort",
  300. "Authentication has not completed successfully");
  301. }
  302. if (succeeded == false) {
  303. return false;
  304. } else if (succeeded == true && commitSucceeded == false) {
  305. // Clean out state
  306. succeeded = false;
  307. cleanState();
  308. user = null;
  309. } else {
  310. // overall authentication succeeded and commit succeeded,
  311. // but someone else's commit failed
  312. logout();
  313. }
  314. return true;
  315. }
  316. /**
  317. * Logout a user.
  318. *
  319. * <p> This method removes the Principals
  320. * that were added by the <code>commit</code> method.
  321. *
  322. * @exception LoginException if the logout fails.
  323. * @return true in all cases since this <code>LoginModule</code>
  324. * should not be ignored.
  325. */
  326. public boolean logout() throws LoginException {
  327. if (subject.isReadOnly()) {
  328. cleanState();
  329. throw new LoginException ("Subject is read-only");
  330. }
  331. subject.getPrincipals().remove(user);
  332. // clean out state
  333. cleanState();
  334. succeeded = false;
  335. commitSucceeded = false;
  336. user = null;
  337. if (logger.debugOn()) {
  338. logger.debug("logout", "Subject is being logged out");
  339. }
  340. return true;
  341. }
  342. /**
  343. * Attempt authentication
  344. *
  345. * @param usePasswdFromSharedState a flag to tell this method whether
  346. * to retrieve the password from the sharedState.
  347. */
  348. private void attemptAuthentication(boolean usePasswdFromSharedState)
  349. throws LoginException {
  350. // get the username and password
  351. getUsernamePassword(usePasswdFromSharedState);
  352. String localPassword = null;
  353. // userCredentials is initialized in login()
  354. if (((localPassword = userCredentials.getProperty(username)) == null) ||
  355. (! localPassword.equals(new String(password)))) {
  356. // username not found or passwords do not match
  357. if (logger.debugOn()) {
  358. logger.debug("login", "Invalid username or password");
  359. }
  360. throw new FailedLoginException("Invalid username or password");
  361. }
  362. // Save the username and password in the shared state
  363. // only if authentication succeeded
  364. if (storePass &&
  365. !sharedState.containsKey(USERNAME_KEY) &&
  366. !sharedState.containsKey(PASSWORD_KEY)) {
  367. sharedState.put(USERNAME_KEY, username);
  368. sharedState.put(PASSWORD_KEY, password);
  369. }
  370. // Create a new user principal
  371. user = new JMXPrincipal(username);
  372. if (logger.debugOn()) {
  373. logger.debug("login",
  374. "User '" + username + "' successfully validated");
  375. }
  376. }
  377. /*
  378. * Read the password file.
  379. */
  380. private void loadPasswordFile() throws IOException {
  381. BufferedInputStream bis =
  382. new BufferedInputStream(new FileInputStream(passwordFile));
  383. userCredentials = new Properties();
  384. userCredentials.load(bis);
  385. bis.close();
  386. }
  387. /**
  388. * Get the username and password.
  389. * This method does not return any value.
  390. * Instead, it sets global name and password variables.
  391. *
  392. * <p> Also note that this method will set the username and password
  393. * values in the shared state in case subsequent LoginModules
  394. * want to use them via use/tryFirstPass.
  395. *
  396. * @param usePasswdFromSharedState boolean that tells this method whether
  397. * to retrieve the password from the sharedState.
  398. */
  399. private void getUsernamePassword(boolean usePasswdFromSharedState)
  400. throws LoginException {
  401. if (usePasswdFromSharedState) {
  402. // use the password saved by the first module in the stack
  403. username = (String)sharedState.get(USERNAME_KEY);
  404. password = (char[])sharedState.get(PASSWORD_KEY);
  405. return;
  406. }
  407. // acquire username and password
  408. if (callbackHandler == null)
  409. throw new LoginException("Error: no CallbackHandler available " +
  410. "to garner authentication information from the user");
  411. Callback[] callbacks = new Callback[2];
  412. callbacks[0] = new NameCallback("username");
  413. callbacks[1] = new PasswordCallback("password", false);
  414. try {
  415. callbackHandler.handle(callbacks);
  416. username = ((NameCallback)callbacks[0]).getName();
  417. char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword();
  418. password = new char[tmpPassword.length];
  419. System.arraycopy(tmpPassword, 0,
  420. password, 0, tmpPassword.length);
  421. ((PasswordCallback)callbacks[1]).clearPassword();
  422. } catch (IOException ioe) {
  423. LoginException le = new LoginException(ioe.toString());
  424. throw (LoginException) EnvHelp.initCause(le, ioe);
  425. } catch (UnsupportedCallbackException uce) {
  426. LoginException le = new LoginException(
  427. "Error: " + uce.getCallback().toString() +
  428. " not available to garner authentication " +
  429. "information from the user");
  430. throw (LoginException) EnvHelp.initCause(le, uce);
  431. }
  432. }
  433. /**
  434. * Clean out state because of a failed authentication attempt
  435. */
  436. private void cleanState() {
  437. username = null;
  438. if (password != null) {
  439. Arrays.fill(password, ' ');
  440. password = null;
  441. }
  442. if (clearPass) {
  443. sharedState.remove(USERNAME_KEY);
  444. sharedState.remove(PASSWORD_KEY);
  445. }
  446. }
  447. }