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