0001: /*
0002: * Copyright 2005-2006 Sun Microsystems, Inc. All Rights Reserved.
0003: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
0004: *
0005: * This code is free software; you can redistribute it and/or modify it
0006: * under the terms of the GNU General Public License version 2 only, as
0007: * published by the Free Software Foundation. Sun designates this
0008: * particular file as subject to the "Classpath" exception as provided
0009: * by Sun in the LICENSE file that accompanied this code.
0010: *
0011: * This code is distributed in the hope that it will be useful, but WITHOUT
0012: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0013: * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
0014: * version 2 for more details (a copy is included in the LICENSE file that
0015: * accompanied this code).
0016: *
0017: * You should have received a copy of the GNU General Public License version
0018: * 2 along with this work; if not, write to the Free Software Foundation,
0019: * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
0020: *
0021: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
0022: * CA 95054 USA or visit www.sun.com if you need additional information or
0023: * have any questions.
0024: */
0025:
0026: package com.sun.security.auth.module;
0027:
0028: import java.io.IOException;
0029: import java.security.AccessController;
0030: import java.net.SocketPermission;
0031: import java.security.Principal;
0032: import java.security.PrivilegedAction;
0033: import java.util.Arrays;
0034: import java.util.Hashtable;
0035: import java.util.Iterator;
0036: import java.util.Map;
0037: import java.util.ResourceBundle;
0038: import java.util.regex.Matcher;
0039: import java.util.regex.Pattern;
0040: import java.util.Set;
0041:
0042: import javax.naming.*;
0043: import javax.naming.directory.*;
0044: import javax.naming.ldap.*;
0045: import javax.security.auth.*;
0046: import javax.security.auth.callback.*;
0047: import javax.security.auth.login.*;
0048: import javax.security.auth.spi.*;
0049:
0050: import com.sun.security.auth.LdapPrincipal;
0051: import com.sun.security.auth.UserPrincipal;
0052:
0053: import sun.security.util.AuthResources;
0054:
0055: /**
0056: * This {@link LoginModule} performs LDAP-based authentication.
0057: * A username and password is verified against the corresponding user
0058: * credentials stored in an LDAP directory.
0059: * This module requires the supplied {@link CallbackHandler} to support a
0060: * {@link NameCallback} and a {@link PasswordCallback}.
0061: * If authentication is successful then a new {@link LdapPrincipal} is created
0062: * using the user's distinguished name and a new {@link UserPrincipal} is
0063: * created using the user's username and both are associated
0064: * with the current {@link Subject}.
0065: *
0066: * <p> This module operates in one of three modes: <i>search-first</i>,
0067: * <i>authentication-first</i> or <i>authentication-only</i>.
0068: * A mode is selected by specifying a particular set of options.
0069: *
0070: * <p> In search-first mode, the LDAP directory is searched to determine the
0071: * user's distinguished name and then authentication is attempted.
0072: * An (anonymous) search is performed using the supplied username in
0073: * conjunction with a specified search filter.
0074: * If successful then authentication is attempted using the user's
0075: * distinguished name and the supplied password.
0076: * To enable this mode, set the <code>userFilter</code> option and omit the
0077: * <code>authIdentity</code> option.
0078: * Use search-first mode when the user's distinguished name is not
0079: * known in advance.
0080: *
0081: * <p> In authentication-first mode, authentication is attempted using the
0082: * supplied username and password and then the LDAP directory is searched.
0083: * If authentication is successful then a search is performed using the
0084: * supplied username in conjunction with a specified search filter.
0085: * To enable this mode, set the <code>authIdentity</code> and the
0086: * <code>userFilter</code> options.
0087: * Use authentication-first mode when accessing an LDAP directory
0088: * that has been configured to disallow anonymous searches.
0089: *
0090: * <p> In authentication-only mode, authentication is attempted using the
0091: * supplied username and password. The LDAP directory is not searched because
0092: * the user's distinguished name is already known.
0093: * To enable this mode, set the <code>authIdentity</code> option to a valid
0094: * distinguished name and omit the <code>userFilter</code> option.
0095: * Use authentication-only mode when the user's distinguished name is
0096: * known in advance.
0097: *
0098: * <p> The following option is mandatory and must be specified in this
0099: * module's login {@link Configuration}:
0100: * <dl><dt></dt><dd>
0101: * <dl>
0102: * <dt> <code>userProvider=<b>ldap_urls</b></code>
0103: * </dt>
0104: * <dd> This option identifies the LDAP directory that stores user entries.
0105: * <b>ldap_urls</b> is a list of space-separated LDAP URLs
0106: * (<a href="http://www.ietf.org/rfc/rfc2255.txt">RFC 2255</a>)
0107: * that identifies the LDAP server to use and the position in
0108: * its directory tree where user entries are located.
0109: * When several LDAP URLs are specified then each is attempted,
0110: * in turn, until the first successful connection is established.
0111: * Spaces in the distinguished name component of the URL must be escaped
0112: * using the standard mechanism of percent character ('<code>%</code>')
0113: * followed by two hexadecimal digits (see {@link java.net.URI}).
0114: * Query components must also be omitted from the URL.
0115: *
0116: * <p>
0117: * Automatic discovery of the LDAP server via DNS
0118: * (<a href="http://www.ietf.org/rfc/rfc2782.txt">RFC 2782</a>)
0119: * is supported (once DNS has been configured to support such a service).
0120: * It is enabled by omitting the hostname and port number components from
0121: * the LDAP URL. </dd>
0122: * </dl></dl>
0123: *
0124: * <p> This module also recognizes the following optional {@link Configuration}
0125: * options:
0126: * <dl><dt></dt><dd>
0127: * <dl>
0128: * <dt> <code>userFilter=<b>ldap_filter</b></code> </dt>
0129: * <dd> This option specifies the search filter to use to locate a user's
0130: * entry in the LDAP directory. It is used to determine a user's
0131: * distinguished name.
0132: * <code><b>ldap_filter</b></code> is an LDAP filter string
0133: * (<a href="http://www.ietf.org/rfc/rfc2254.txt">RFC 2254</a>).
0134: * If it contains the special token "<code><b>{USERNAME}</b></code>"
0135: * then that token will be replaced with the supplied username value
0136: * before the filter is used to search the directory. </dd>
0137: *
0138: * <dt> <code>authIdentity=<b>auth_id</b></code> </dt>
0139: * <dd> This option specifies the identity to use when authenticating a user
0140: * to the LDAP directory.
0141: * <code><b>auth_id</b></code> may be an LDAP distinguished name string
0142: * (<a href="http://www.ietf.org/rfc/rfc2253.txt">RFC 2253</a>) or some
0143: * other string name.
0144: * It must contain the special token "<code><b>{USERNAME}</b></code>"
0145: * which will be replaced with the supplied username value before the
0146: * name is used for authentication.
0147: * Note that if this option does not contain a distinguished name then
0148: * the <code>userFilter</code> option must also be specified. </dd>
0149: *
0150: * <dt> <code>authzIdentity=<b>authz_id</b></code> </dt>
0151: * <dd> This option specifies an authorization identity for the user.
0152: * <code><b>authz_id</b></code> is any string name.
0153: * If it comprises a single special token with curly braces then
0154: * that token is treated as a attribute name and will be replaced with a
0155: * single value of that attribute from the user's LDAP entry.
0156: * If the attribute cannot be found then the option is ignored.
0157: * When this option is supplied and the user has been successfully
0158: * authenticated then an additional {@link UserPrincipal}
0159: * is created using the authorization identity and it is assocated with
0160: * the current {@link Subject}. </dd>
0161: *
0162: * <dt> <code>useSSL</code> </dt>
0163: * <dd> if <code>false</code>, this module does not establish an SSL connection
0164: * to the LDAP server before attempting authentication. SSL is used to
0165: * protect the privacy of the user's password because it is transmitted
0166: * in the clear over LDAP.
0167: * By default, this module uses SSL. </dd>
0168: *
0169: * <dt> <code>useFirstPass</code> </dt>
0170: * <dd> if <code>true</code>, this module retrieves the username and password
0171: * from the module's shared state, using "javax.security.auth.login.name"
0172: * and "javax.security.auth.login.password" as the respective keys. The
0173: * retrieved values are used for authentication. If authentication fails,
0174: * no attempt for a retry is made, and the failure is reported back to
0175: * the calling application.</dd>
0176: *
0177: * <dt> <code>tryFirstPass</code> </dt>
0178: * <dd> if <code>true</code>, this module retrieves the username and password
0179: * from the module's shared state, using "javax.security.auth.login.name"
0180: * and "javax.security.auth.login.password" as the respective keys. The
0181: * retrieved values are used for authentication. If authentication fails,
0182: * the module uses the {@link CallbackHandler} to retrieve a new username
0183: * and password, and another attempt to authenticate is made. If the
0184: * authentication fails, the failure is reported back to the calling
0185: * application.</dd>
0186: *
0187: * <dt> <code>storePass</code> </dt>
0188: * <dd> if <code>true</code>, this module stores the username and password
0189: * obtained from the {@link CallbackHandler} in the module's shared state,
0190: * using
0191: * "javax.security.auth.login.name" and
0192: * "javax.security.auth.login.password" as the respective keys. This is
0193: * not performed if existing values already exist for the username and
0194: * password in the shared state, or if authentication fails.</dd>
0195: *
0196: * <dt> <code>clearPass</code> </dt>
0197: * <dd> if <code>true</code>, this module clears the username and password
0198: * stored in the module's shared state after both phases of authentication
0199: * (login and commit) have completed.</dd>
0200: *
0201: * <dt> <code>debug</code> </dt>
0202: * <dd> if <code>true</code>, debug messages are displayed on the standard
0203: * output stream.
0204: * </dl>
0205: * </dl>
0206: *
0207: * <p>
0208: * Arbitrary
0209: * <a href="{@docRoot}/../../../../../technotes/guides/jndi/jndi-ldap-gl.html#PROP">JNDI properties</a>
0210: * may also be specified in the {@link Configuration}.
0211: * They are added to the environment and passed to the LDAP provider.
0212: * Note that the following four JNDI properties are set by this module directly
0213: * and are ignored if also present in the configuration:
0214: * <ul>
0215: * <li> <code>java.naming.provider.url</code>
0216: * <li> <code>java.naming.security.principal</code>
0217: * <li> <code>java.naming.security.credentials</code>
0218: * <li> <code>java.naming.security.protocol</code>
0219: * </ul>
0220: *
0221: * <p>
0222: * Three sample {@link Configuration}s are shown below.
0223: * The first one activates search-first mode. It identifies the LDAP server
0224: * and specifies that users' entries be located by their <code>uid</code> and
0225: * <code>objectClass</code> attributes. It also specifies that an identity
0226: * based on the user's <code>employeeNumber</code> attribute should be created.
0227: * The second one activates authentication-first mode. It requests that the
0228: * LDAP server be located dynamically, that authentication be performed using
0229: * the supplied username directly but without the protection of SSL and that
0230: * users' entries be located by one of three naming attributes and their
0231: * <code>objectClass</code> attribute.
0232: * The third one activates authentication-only mode. It identifies alternative
0233: * LDAP servers, it specifies the distinguished name to use for
0234: * authentication and a fixed identity to use for authorization. No directory
0235: * search is performed.
0236: *
0237: * <pre>
0238: *
0239: * ExampleApplication {
0240: * com.sun.security.auth.module.LdapLoginModule REQUIRED
0241: * userProvider="ldap://ldap-svr/ou=people,dc=example,dc=com"
0242: * userFilter="(&(uid={USERNAME})(objectClass=inetOrgPerson))"
0243: * authzIdentity="{EMPLOYEENUMBER}"
0244: * debug=true;
0245: * };
0246: *
0247: * ExampleApplication {
0248: * com.sun.security.auth.module.LdapLoginModule REQUIRED
0249: * userProvider="ldap:///cn=users,dc=example,dc=com"
0250: * authIdentity="{USERNAME}"
0251: * userFilter="(&(|(samAccountName={USERNAME})(userPrincipalName={USERNAME})(cn={USERNAME}))(objectClass=user))"
0252: * useSSL=false
0253: * debug=true;
0254: * };
0255: *
0256: * ExampleApplication {
0257: * com.sun.security.auth.module.LdapLoginModule REQUIRED
0258: * userProvider="ldap://ldap-svr1 ldap://ldap-svr2"
0259: * authIdentity="cn={USERNAME},ou=people,dc=example,dc=com"
0260: * authzIdentity="staff"
0261: * debug=true;
0262: * };
0263: *
0264: * </pre>
0265: *
0266: * <dl>
0267: * <dt><b>Note:</b> </dt>
0268: * <dd>When a {@link SecurityManager} is active then an application
0269: * that creates a {@link LoginContext} and uses a {@link LoginModule}
0270: * must be granted certain permissions.
0271: * <p>
0272: * If the application creates a login context using an <em>installed</em>
0273: * {@link Configuration} then the application must be granted the
0274: * {@link AuthPermission} to create login contexts.
0275: * For example, the following security policy allows an application in
0276: * the user's current directory to instantiate <em>any</em> login context:
0277: * <pre>
0278: *
0279: * grant codebase "file:${user.dir}/" {
0280: * permission javax.security.auth.AuthPermission "createLoginContext.*";
0281: * };
0282: * </pre>
0283: *
0284: * Alternatively, if the application creates a login context using a
0285: * <em>caller-specified</em> {@link Configuration} then the application
0286: * must be granted the permissions required by the {@link LoginModule}.
0287: * <em>This</em> module requires the following two permissions:
0288: * <p>
0289: * <ul>
0290: * <li> The {@link SocketPermission} to connect to an LDAP server.
0291: * <li> The {@link AuthPermission} to modify the set of {@link Principal}s
0292: * associated with a {@link Subject}.
0293: * </ul>
0294: * <p>
0295: * For example, the following security policy grants an application in the
0296: * user's current directory all the permissions required by this module:
0297: * <pre>
0298: *
0299: * grant codebase "file:${user.dir}/" {
0300: * permission java.net.SocketPermission "*:389", "connect";
0301: * permission java.net.SocketPermission "*:636", "connect";
0302: * permission javax.security.auth.AuthPermission "modifyPrincipals";
0303: * };
0304: * </pre>
0305: * </dd>
0306: * </dl>
0307: *
0308: * @since 1.6
0309: */
0310: public class LdapLoginModule implements LoginModule {
0311:
0312: // Use the default classloader for this class to load the prompt strings.
0313: private static final ResourceBundle rb = AccessController
0314: .doPrivileged(new PrivilegedAction<ResourceBundle>() {
0315: public ResourceBundle run() {
0316: return ResourceBundle
0317: .getBundle("sun.security.util.AuthResources");
0318: }
0319: });
0320:
0321: // Keys to retrieve the stored username and password
0322: private static final String USERNAME_KEY = "javax.security.auth.login.name";
0323: private static final String PASSWORD_KEY = "javax.security.auth.login.password";
0324:
0325: // Option names
0326: private static final String USER_PROVIDER = "userProvider";
0327: private static final String USER_FILTER = "userFilter";
0328: private static final String AUTHC_IDENTITY = "authIdentity";
0329: private static final String AUTHZ_IDENTITY = "authzIdentity";
0330:
0331: // Used for the username token replacement
0332: private static final String USERNAME_TOKEN = "{USERNAME}";
0333: private static final Pattern USERNAME_PATTERN = Pattern
0334: .compile("\\{USERNAME\\}");
0335:
0336: // Configurable options
0337: private String userProvider;
0338: private String userFilter;
0339: private String authcIdentity;
0340: private String authzIdentity;
0341: private String authzIdentityAttr = null;
0342: private boolean useSSL = true;
0343: private boolean authFirst = false;
0344: private boolean authOnly = false;
0345: private boolean useFirstPass = false;
0346: private boolean tryFirstPass = false;
0347: private boolean storePass = false;
0348: private boolean clearPass = false;
0349: private boolean debug = false;
0350:
0351: // Authentication status
0352: private boolean succeeded = false;
0353: private boolean commitSucceeded = false;
0354:
0355: // Supplied username and password
0356: private String username;
0357: private char[] password;
0358:
0359: // User's identities
0360: private LdapPrincipal ldapPrincipal;
0361: private UserPrincipal userPrincipal;
0362: private UserPrincipal authzPrincipal;
0363:
0364: // Initial state
0365: private Subject subject;
0366: private CallbackHandler callbackHandler;
0367: private Map sharedState;
0368: private Map<String, ?> options;
0369: private LdapContext ctx;
0370: private Matcher identityMatcher = null;
0371: private Matcher filterMatcher = null;
0372: private Hashtable ldapEnvironment;
0373: private SearchControls constraints = null;
0374:
0375: /**
0376: * Initialize this <code>LoginModule</code>.
0377: *
0378: * @param subject the <code>Subject</code> to be authenticated.
0379: * @param callbackHandler a <code>CallbackHandler</code> to acquire the
0380: * username and password.
0381: * @param sharedState shared <code>LoginModule</code> state.
0382: * @param options options specified in the login
0383: * <code>Configuration</code> for this particular
0384: * <code>LoginModule</code>.
0385: */
0386: public void initialize(Subject subject,
0387: CallbackHandler callbackHandler,
0388: Map<String, ?> sharedState, Map<String, ?> options) {
0389:
0390: this .subject = subject;
0391: this .callbackHandler = callbackHandler;
0392: this .sharedState = sharedState;
0393: this .options = options;
0394:
0395: ldapEnvironment = new Hashtable(9);
0396: ldapEnvironment.put(Context.INITIAL_CONTEXT_FACTORY,
0397: "com.sun.jndi.ldap.LdapCtxFactory");
0398:
0399: // Add any JNDI properties to the environment
0400: for (String key : options.keySet()) {
0401: if (key.indexOf(".") > -1) {
0402: ldapEnvironment.put(key, options.get(key));
0403: }
0404: }
0405:
0406: // initialize any configured options
0407:
0408: userProvider = (String) options.get(USER_PROVIDER);
0409: if (userProvider != null) {
0410: ldapEnvironment.put(Context.PROVIDER_URL, userProvider);
0411: }
0412:
0413: authcIdentity = (String) options.get(AUTHC_IDENTITY);
0414: if (authcIdentity != null
0415: && (authcIdentity.indexOf(USERNAME_TOKEN) != -1)) {
0416: identityMatcher = USERNAME_PATTERN.matcher(authcIdentity);
0417: }
0418:
0419: userFilter = (String) options.get(USER_FILTER);
0420: if (userFilter != null) {
0421: if (userFilter.indexOf(USERNAME_TOKEN) != -1) {
0422: filterMatcher = USERNAME_PATTERN.matcher(userFilter);
0423: }
0424: constraints = new SearchControls();
0425: constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
0426: constraints.setReturningAttributes(new String[0]); //return no attrs
0427: constraints.setReturningObjFlag(true); // to get the full DN
0428: }
0429:
0430: authzIdentity = (String) options.get(AUTHZ_IDENTITY);
0431: if (authzIdentity != null && authzIdentity.startsWith("{")
0432: && authzIdentity.endsWith("}")) {
0433: if (constraints != null) {
0434: authzIdentityAttr = authzIdentity.substring(1,
0435: authzIdentity.length() - 1);
0436: constraints
0437: .setReturningAttributes(new String[] { authzIdentityAttr });
0438: }
0439: authzIdentity = null; // set later, from the specified attribute
0440: }
0441:
0442: // determine mode
0443: if (authcIdentity != null) {
0444: if (userFilter != null) {
0445: authFirst = true; // authentication-first mode
0446: } else {
0447: authOnly = true; // authentication-only mode
0448: }
0449: }
0450:
0451: if ("false".equalsIgnoreCase((String) options.get("useSSL"))) {
0452: useSSL = false;
0453: ldapEnvironment.remove(Context.SECURITY_PROTOCOL);
0454: } else {
0455: ldapEnvironment.put(Context.SECURITY_PROTOCOL, "ssl");
0456: }
0457:
0458: tryFirstPass = "true".equalsIgnoreCase((String) options
0459: .get("tryFirstPass"));
0460:
0461: useFirstPass = "true".equalsIgnoreCase((String) options
0462: .get("useFirstPass"));
0463:
0464: storePass = "true".equalsIgnoreCase((String) options
0465: .get("storePass"));
0466:
0467: clearPass = "true".equalsIgnoreCase((String) options
0468: .get("clearPass"));
0469:
0470: debug = "true".equalsIgnoreCase((String) options.get("debug"));
0471:
0472: if (debug) {
0473: if (authFirst) {
0474: System.out.println("\t\t[LdapLoginModule] "
0475: + "authentication-first mode; "
0476: + (useSSL ? "SSL enabled" : "SSL disabled"));
0477: } else if (authOnly) {
0478: System.out.println("\t\t[LdapLoginModule] "
0479: + "authentication-only mode; "
0480: + (useSSL ? "SSL enabled" : "SSL disabled"));
0481: } else {
0482: System.out.println("\t\t[LdapLoginModule] "
0483: + "search-first mode; "
0484: + (useSSL ? "SSL enabled" : "SSL disabled"));
0485: }
0486: }
0487: }
0488:
0489: /**
0490: * Begin user authentication.
0491: *
0492: * <p> Acquire the user's credentials and verify them against the
0493: * specified LDAP directory.
0494: *
0495: * @return true always, since this <code>LoginModule</code>
0496: * should not be ignored.
0497: * @exception FailedLoginException if the authentication fails.
0498: * @exception LoginException if this <code>LoginModule</code>
0499: * is unable to perform the authentication.
0500: */
0501: public boolean login() throws LoginException {
0502:
0503: if (userProvider == null) {
0504: throw new LoginException(
0505: "Unable to locate the LDAP directory service");
0506: }
0507:
0508: if (debug) {
0509: System.out.println("\t\t[LdapLoginModule] user provider: "
0510: + userProvider);
0511: }
0512:
0513: // attempt the authentication
0514: if (tryFirstPass) {
0515:
0516: try {
0517: // attempt the authentication by getting the
0518: // username and password from shared state
0519: attemptAuthentication(true);
0520:
0521: // authentication succeeded
0522: succeeded = true;
0523: if (debug) {
0524: System.out.println("\t\t[LdapLoginModule] "
0525: + "tryFirstPass succeeded");
0526: }
0527: return true;
0528:
0529: } catch (LoginException le) {
0530: // authentication failed -- try again below by prompting
0531: cleanState();
0532: if (debug) {
0533: System.out.println("\t\t[LdapLoginModule] "
0534: + "tryFirstPass failed: " + le.toString());
0535: }
0536: }
0537:
0538: } else if (useFirstPass) {
0539:
0540: try {
0541: // attempt the authentication by getting the
0542: // username and password from shared state
0543: attemptAuthentication(true);
0544:
0545: // authentication succeeded
0546: succeeded = true;
0547: if (debug) {
0548: System.out.println("\t\t[LdapLoginModule] "
0549: + "useFirstPass succeeded");
0550: }
0551: return true;
0552:
0553: } catch (LoginException le) {
0554: // authentication failed
0555: cleanState();
0556: if (debug) {
0557: System.out.println("\t\t[LdapLoginModule] "
0558: + "useFirstPass failed");
0559: }
0560: throw le;
0561: }
0562: }
0563:
0564: // attempt the authentication by prompting for the username and pwd
0565: try {
0566: attemptAuthentication(false);
0567:
0568: // authentication succeeded
0569: succeeded = true;
0570: if (debug) {
0571: System.out.println("\t\t[LdapLoginModule] "
0572: + "authentication succeeded");
0573: }
0574: return true;
0575:
0576: } catch (LoginException le) {
0577: cleanState();
0578: if (debug) {
0579: System.out.println("\t\t[LdapLoginModule] "
0580: + "authentication failed");
0581: }
0582: throw le;
0583: }
0584: }
0585:
0586: /**
0587: * Complete user authentication.
0588: *
0589: * <p> This method is called if the LoginContext's
0590: * overall authentication succeeded
0591: * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
0592: * succeeded).
0593: *
0594: * <p> If this LoginModule's own authentication attempt
0595: * succeeded (checked by retrieving the private state saved by the
0596: * <code>login</code> method), then this method associates an
0597: * <code>LdapPrincipal</code> and one or more <code>UserPrincipal</code>s
0598: * with the <code>Subject</code> located in the
0599: * <code>LoginModule</code>. If this LoginModule's own
0600: * authentication attempted failed, then this method removes
0601: * any state that was originally saved.
0602: *
0603: * @exception LoginException if the commit fails
0604: * @return true if this LoginModule's own login and commit
0605: * attempts succeeded, or false otherwise.
0606: */
0607: public boolean commit() throws LoginException {
0608:
0609: if (succeeded == false) {
0610: return false;
0611: } else {
0612: if (subject.isReadOnly()) {
0613: cleanState();
0614: throw new LoginException("Subject is read-only");
0615: }
0616: // add Principals to the Subject
0617: Set<Principal> principals = subject.getPrincipals();
0618: if (!principals.contains(ldapPrincipal)) {
0619: principals.add(ldapPrincipal);
0620: }
0621: if (debug) {
0622: System.out.println("\t\t[LdapLoginModule] "
0623: + "added LdapPrincipal \"" + ldapPrincipal
0624: + "\" to Subject");
0625: }
0626:
0627: if (!principals.contains(userPrincipal)) {
0628: principals.add(userPrincipal);
0629: }
0630: if (debug) {
0631: System.out.println("\t\t[LdapLoginModule] "
0632: + "added UserPrincipal \"" + userPrincipal
0633: + "\" to Subject");
0634: }
0635:
0636: if (authzPrincipal != null
0637: && (!principals.contains(authzPrincipal))) {
0638: principals.add(authzPrincipal);
0639:
0640: if (debug) {
0641: System.out.println("\t\t[LdapLoginModule] "
0642: + "added UserPrincipal \"" + authzPrincipal
0643: + "\" to Subject");
0644: }
0645: }
0646: }
0647: // in any case, clean out state
0648: cleanState();
0649: commitSucceeded = true;
0650: return true;
0651: }
0652:
0653: /**
0654: * Abort user authentication.
0655: *
0656: * <p> This method is called if the overall authentication failed.
0657: * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
0658: * did not succeed).
0659: *
0660: * <p> If this LoginModule's own authentication attempt
0661: * succeeded (checked by retrieving the private state saved by the
0662: * <code>login</code> and <code>commit</code> methods),
0663: * then this method cleans up any state that was originally saved.
0664: *
0665: * @exception LoginException if the abort fails.
0666: * @return false if this LoginModule's own login and/or commit attempts
0667: * failed, and true otherwise.
0668: */
0669: public boolean abort() throws LoginException {
0670: if (debug)
0671: System.out.println("\t\t[LdapLoginModule] "
0672: + "aborted authentication");
0673:
0674: if (succeeded == false) {
0675: return false;
0676: } else if (succeeded == true && commitSucceeded == false) {
0677:
0678: // Clean out state
0679: succeeded = false;
0680: cleanState();
0681:
0682: ldapPrincipal = null;
0683: userPrincipal = null;
0684: authzPrincipal = null;
0685: } else {
0686: // overall authentication succeeded and commit succeeded,
0687: // but someone else's commit failed
0688: logout();
0689: }
0690: return true;
0691: }
0692:
0693: /**
0694: * Logout a user.
0695: *
0696: * <p> This method removes the Principals
0697: * that were added by the <code>commit</code> method.
0698: *
0699: * @exception LoginException if the logout fails.
0700: * @return true in all cases since this <code>LoginModule</code>
0701: * should not be ignored.
0702: */
0703: public boolean logout() throws LoginException {
0704: if (subject.isReadOnly()) {
0705: cleanState();
0706: throw new LoginException("Subject is read-only");
0707: }
0708: Set<Principal> principals = subject.getPrincipals();
0709: principals.remove(ldapPrincipal);
0710: principals.remove(userPrincipal);
0711: if (authzIdentity != null) {
0712: principals.remove(authzPrincipal);
0713: }
0714:
0715: // clean out state
0716: cleanState();
0717: succeeded = false;
0718: commitSucceeded = false;
0719:
0720: ldapPrincipal = null;
0721: userPrincipal = null;
0722: authzPrincipal = null;
0723:
0724: if (debug) {
0725: System.out
0726: .println("\t\t[LdapLoginModule] logged out Subject");
0727: }
0728: return true;
0729: }
0730:
0731: /**
0732: * Attempt authentication
0733: *
0734: * @param getPasswdFromSharedState boolean that tells this method whether
0735: * to retrieve the password from the sharedState.
0736: * @exception LoginException if the authentication attempt fails.
0737: */
0738: private void attemptAuthentication(boolean getPasswdFromSharedState)
0739: throws LoginException {
0740:
0741: // first get the username and password
0742: getUsernamePassword(getPasswdFromSharedState);
0743:
0744: if (password == null || password.length == 0) {
0745: throw (LoginException) new FailedLoginException(
0746: "No password was supplied");
0747: }
0748:
0749: String dn = "";
0750:
0751: if (authFirst || authOnly) {
0752:
0753: String id = replaceUsernameToken(identityMatcher,
0754: authcIdentity);
0755:
0756: // Prepare to bind using user's username and password
0757: ldapEnvironment.put(Context.SECURITY_CREDENTIALS, password);
0758: ldapEnvironment.put(Context.SECURITY_PRINCIPAL, id);
0759:
0760: if (debug) {
0761: System.out.println("\t\t[LdapLoginModule] "
0762: + "attempting to authenticate user: "
0763: + username);
0764: }
0765:
0766: try {
0767: // Connect to the LDAP server (using simple bind)
0768: ctx = new InitialLdapContext(ldapEnvironment, null);
0769:
0770: } catch (NamingException e) {
0771: throw (LoginException) new FailedLoginException(
0772: "Cannot bind to LDAP server").initCause(e);
0773: }
0774:
0775: // Authentication has succeeded
0776:
0777: // Locate the user's distinguished name
0778: if (userFilter != null) {
0779: dn = findUserDN(ctx);
0780: } else {
0781: dn = id;
0782: }
0783:
0784: } else {
0785:
0786: try {
0787: // Connect to the LDAP server (using anonymous bind)
0788: ctx = new InitialLdapContext(ldapEnvironment, null);
0789:
0790: } catch (NamingException e) {
0791: throw (LoginException) new FailedLoginException(
0792: "Cannot connect to LDAP server").initCause(e);
0793: }
0794:
0795: // Locate the user's distinguished name
0796: dn = findUserDN(ctx);
0797:
0798: try {
0799:
0800: // Prepare to bind using user's distinguished name and password
0801: ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION,
0802: "simple");
0803: ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
0804: ctx.addToEnvironment(Context.SECURITY_CREDENTIALS,
0805: password);
0806:
0807: if (debug) {
0808: System.out.println("\t\t[LdapLoginModule] "
0809: + "attempting to authenticate user: "
0810: + username);
0811: }
0812: // Connect to the LDAP server (using simple bind)
0813: ctx.reconnect(null);
0814:
0815: // Authentication has succeeded
0816:
0817: } catch (NamingException e) {
0818: throw (LoginException) new FailedLoginException(
0819: "Cannot bind to LDAP server").initCause(e);
0820: }
0821: }
0822:
0823: // Save input as shared state only if authentication succeeded
0824: if (storePass && !sharedState.containsKey(USERNAME_KEY)
0825: && !sharedState.containsKey(PASSWORD_KEY)) {
0826: sharedState.put(USERNAME_KEY, username);
0827: sharedState.put(PASSWORD_KEY, password);
0828: }
0829:
0830: // Create the user principals
0831: userPrincipal = new UserPrincipal(username);
0832: if (authzIdentity != null) {
0833: authzPrincipal = new UserPrincipal(authzIdentity);
0834: }
0835:
0836: try {
0837:
0838: ldapPrincipal = new LdapPrincipal(dn);
0839:
0840: } catch (InvalidNameException e) {
0841: if (debug) {
0842: System.out.println("\t\t[LdapLoginModule] "
0843: + "cannot create LdapPrincipal: bad DN");
0844: }
0845: throw (LoginException) new FailedLoginException(
0846: "Cannot create LdapPrincipal").initCause(e);
0847: }
0848: }
0849:
0850: /**
0851: * Search for the user's entry.
0852: * Determine the distinguished name of the user's entry and optionally
0853: * an authorization identity for the user.
0854: *
0855: * @param ctx an LDAP context to use for the search
0856: * @return the user's distinguished name or an empty string if none
0857: * was found.
0858: * @exception LoginException if the user's entry cannot be found.
0859: */
0860: private String findUserDN(LdapContext ctx) throws LoginException {
0861:
0862: String userDN = "";
0863:
0864: // Locate the user's LDAP entry
0865: if (userFilter != null) {
0866: if (debug) {
0867: System.out.println("\t\t[LdapLoginModule] "
0868: + "searching for entry belonging to user: "
0869: + username);
0870: }
0871: } else {
0872: if (debug) {
0873: System.out.println("\t\t[LdapLoginModule] "
0874: + "cannot search for entry belonging to user: "
0875: + username);
0876: }
0877: throw (LoginException) new FailedLoginException(
0878: "Cannot find user's LDAP entry");
0879: }
0880:
0881: try {
0882: NamingEnumeration<SearchResult> results = ctx.search("",
0883: replaceUsernameToken(filterMatcher, userFilter),
0884: constraints);
0885:
0886: // Extract the distinguished name of the user's entry
0887: // (Use the first entry if more than one is returned)
0888: if (results.hasMore()) {
0889: SearchResult entry = results.next();
0890:
0891: // %%% - use the SearchResult.getNameInNamespace method
0892: // available in JDK 1.5 and later.
0893: // (can remove call to constraints.setReturningObjFlag)
0894: userDN = ((Context) entry.getObject())
0895: .getNameInNamespace();
0896:
0897: if (debug) {
0898: System.out
0899: .println("\t\t[LdapLoginModule] found entry: "
0900: + userDN);
0901: }
0902:
0903: // Extract a value from user's authorization identity attribute
0904: if (authzIdentityAttr != null) {
0905: Attribute attr = entry.getAttributes().get(
0906: authzIdentityAttr);
0907: if (attr != null) {
0908: Object val = attr.get();
0909: if (val instanceof String) {
0910: authzIdentity = (String) val;
0911: }
0912: }
0913: }
0914:
0915: results.close();
0916:
0917: } else {
0918: // Bad username
0919: if (debug) {
0920: System.out
0921: .println("\t\t[LdapLoginModule] user's entry "
0922: + "not found");
0923: }
0924: }
0925:
0926: } catch (NamingException e) {
0927: // ignore
0928: }
0929:
0930: if (userDN.equals("")) {
0931: throw (LoginException) new FailedLoginException(
0932: "Cannot find user's LDAP entry");
0933: } else {
0934: return userDN;
0935: }
0936: }
0937:
0938: /**
0939: * Replace the username token
0940: *
0941: * @param string the target string
0942: * @return the modified string
0943: */
0944: private String replaceUsernameToken(Matcher matcher, String string) {
0945: return matcher != null ? matcher.replaceAll(username) : string;
0946: }
0947:
0948: /**
0949: * Get the username and password.
0950: * This method does not return any value.
0951: * Instead, it sets global name and password variables.
0952: *
0953: * <p> Also note that this method will set the username and password
0954: * values in the shared state in case subsequent LoginModules
0955: * want to use them via use/tryFirstPass.
0956: *
0957: * @param getPasswdFromSharedState boolean that tells this method whether
0958: * to retrieve the password from the sharedState.
0959: * @exception LoginException if the username/password cannot be acquired.
0960: */
0961: private void getUsernamePassword(boolean getPasswdFromSharedState)
0962: throws LoginException {
0963:
0964: if (getPasswdFromSharedState) {
0965: // use the password saved by the first module in the stack
0966: username = (String) sharedState.get(USERNAME_KEY);
0967: password = (char[]) sharedState.get(PASSWORD_KEY);
0968: return;
0969: }
0970:
0971: // prompt for a username and password
0972: if (callbackHandler == null)
0973: throw new LoginException(
0974: "No CallbackHandler available "
0975: + "to acquire authentication information from the user");
0976:
0977: Callback[] callbacks = new Callback[2];
0978: callbacks[0] = new NameCallback(rb.getString("username: "));
0979: callbacks[1] = new PasswordCallback(rb.getString("password: "),
0980: false);
0981:
0982: try {
0983: callbackHandler.handle(callbacks);
0984: username = ((NameCallback) callbacks[0]).getName();
0985: char[] tmpPassword = ((PasswordCallback) callbacks[1])
0986: .getPassword();
0987: password = new char[tmpPassword.length];
0988: System.arraycopy(tmpPassword, 0, password, 0,
0989: tmpPassword.length);
0990: ((PasswordCallback) callbacks[1]).clearPassword();
0991:
0992: } catch (java.io.IOException ioe) {
0993: throw new LoginException(ioe.toString());
0994:
0995: } catch (UnsupportedCallbackException uce) {
0996: throw new LoginException(
0997: "Error: "
0998: + uce.getCallback().toString()
0999: + " not available to acquire authentication information"
1000: + " from the user");
1001: }
1002: }
1003:
1004: /**
1005: * Clean out state because of a failed authentication attempt
1006: */
1007: private void cleanState() {
1008: username = null;
1009: if (password != null) {
1010: Arrays.fill(password, ' ');
1011: password = null;
1012: }
1013: try {
1014: if (ctx != null) {
1015: ctx.close();
1016: }
1017: } catch (NamingException e) {
1018: // ignore
1019: }
1020: ctx = null;
1021:
1022: if (clearPass) {
1023: sharedState.remove(USERNAME_KEY);
1024: sharedState.remove(PASSWORD_KEY);
1025: }
1026: }
1027: }
|