/* * Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.security.auth.module; import java.io.IOException; import java.security.AccessController; import java.net.SocketPermission; import java.security.Principal; import java.security.PrivilegedAction; import java.util.Arrays; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.ResourceBundle; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.Set; import javax.naming.*; import javax.naming.directory.*; import javax.naming.ldap.*; import javax.security.auth.*; import javax.security.auth.callback.*; import javax.security.auth.login.*; import javax.security.auth.spi.*; import com.sun.security.auth.LdapPrincipal; import com.sun.security.auth.UserPrincipal; import sun.security.util.AuthResources; /** * This {@link LoginModule} performs LDAP-based authentication. * A username and password is verified against the corresponding user * credentials stored in an LDAP directory. * This module requires the supplied {@link CallbackHandler} to support a * {@link NameCallback} and a {@link PasswordCallback}. * If authentication is successful then a new {@link LdapPrincipal} is created * using the user's distinguished name and a new {@link UserPrincipal} is * created using the user's username and both are associated * with the current {@link Subject}. * *
This module operates in one of three modes: search-first, * authentication-first or authentication-only. * A mode is selected by specifying a particular set of options. * *
In search-first mode, the LDAP directory is searched to determine the
* user's distinguished name and then authentication is attempted.
* An (anonymous) search is performed using the supplied username in
* conjunction with a specified search filter.
* If successful then authentication is attempted using the user's
* distinguished name and the supplied password.
* To enable this mode, set the userFilter
option and omit the
* authIdentity
option.
* Use search-first mode when the user's distinguished name is not
* known in advance.
*
*
In authentication-first mode, authentication is attempted using the
* supplied username and password and then the LDAP directory is searched.
* If authentication is successful then a search is performed using the
* supplied username in conjunction with a specified search filter.
* To enable this mode, set the authIdentity
and the
* userFilter
options.
* Use authentication-first mode when accessing an LDAP directory
* that has been configured to disallow anonymous searches.
*
*
In authentication-only mode, authentication is attempted using the
* supplied username and password. The LDAP directory is not searched because
* the user's distinguished name is already known.
* To enable this mode, set the authIdentity
option to a valid
* distinguished name and omit the userFilter
option.
* Use authentication-only mode when the user's distinguished name is
* known in advance.
*
*
The following option is mandatory and must be specified in this * module's login {@link Configuration}: *
userProvider=ldap_urls
* %
')
* followed by two hexadecimal digits (see {@link java.net.URI}).
* Query components must also be omitted from the URL.
*
* * Automatic discovery of the LDAP server via DNS * (RFC 2782) * is supported (once DNS has been configured to support such a service). * It is enabled by omitting the hostname and port number components from * the LDAP URL.
This module also recognizes the following optional {@link Configuration} * options: *
userFilter=ldap_filter
ldap_filter
is an LDAP filter string
* (RFC 2254).
* If it contains the special token "{USERNAME}
"
* then that token will be replaced with the supplied username value
* before the filter is used to search the directory. authIdentity=auth_id
auth_id
may be an LDAP distinguished name string
* (RFC 2253) or some
* other string name.
* It must contain the special token "{USERNAME}
"
* which will be replaced with the supplied username value before the
* name is used for authentication.
* Note that if this option does not contain a distinguished name then
* the userFilter
option must also be specified. authzIdentity=authz_id
authz_id
is any string name.
* If it comprises a single special token with curly braces then
* that token is treated as a attribute name and will be replaced with a
* single value of that attribute from the user's LDAP entry.
* If the attribute cannot be found then the option is ignored.
* When this option is supplied and the user has been successfully
* authenticated then an additional {@link UserPrincipal}
* is created using the authorization identity and it is assocated with
* the current {@link Subject}. useSSL
false
, this module does not establish an SSL connection
* to the LDAP server before attempting authentication. SSL is used to
* protect the privacy of the user's password because it is transmitted
* in the clear over LDAP.
* By default, this module uses SSL. useFirstPass
true
, this module retrieves the username and password
* from the module's shared state, using "javax.security.auth.login.name"
* and "javax.security.auth.login.password" as the respective keys. The
* retrieved values are used for authentication. If authentication fails,
* no attempt for a retry is made, and the failure is reported back to
* the calling application.tryFirstPass
true
, this module retrieves the username and password
* from the module's shared state, using "javax.security.auth.login.name"
* and "javax.security.auth.login.password" as the respective keys. The
* retrieved values are used for authentication. If authentication fails,
* the module uses the {@link CallbackHandler} to retrieve a new username
* and password, and another attempt to authenticate is made. If the
* authentication fails, the failure is reported back to the calling
* application.storePass
true
, this module stores the username and password
* obtained from the {@link CallbackHandler} in the module's shared state,
* using
* "javax.security.auth.login.name" and
* "javax.security.auth.login.password" as the respective keys. This is
* not performed if existing values already exist for the username and
* password in the shared state, or if authentication fails.clearPass
true
, this module clears the username and password
* stored in the module's shared state after both phases of authentication
* (login and commit) have completed.debug
true
, debug messages are displayed on the standard
* output stream.
* * Arbitrary * JNDI properties * may also be specified in the {@link Configuration}. * They are added to the environment and passed to the LDAP provider. * Note that the following four JNDI properties are set by this module directly * and are ignored if also present in the configuration: *
java.naming.provider.url
* java.naming.security.principal
* java.naming.security.credentials
* java.naming.security.protocol
*
* Three sample {@link Configuration}s are shown below.
* The first one activates search-first mode. It identifies the LDAP server
* and specifies that users' entries be located by their uid
and
* objectClass
attributes. It also specifies that an identity
* based on the user's employeeNumber
attribute should be created.
* The second one activates authentication-first mode. It requests that the
* LDAP server be located dynamically, that authentication be performed using
* the supplied username directly but without the protection of SSL and that
* users' entries be located by one of three naming attributes and their
* objectClass
attribute.
* The third one activates authentication-only mode. It identifies alternative
* LDAP servers, it specifies the distinguished name to use for
* authentication and a fixed identity to use for authorization. No directory
* search is performed.
*
*
* * ExampleApplication { * com.sun.security.auth.module.LdapLoginModule REQUIRED * userProvider="ldap://ldap-svr/ou=people,dc=example,dc=com" * userFilter="(&(uid={USERNAME})(objectClass=inetOrgPerson))" * authzIdentity="{EMPLOYEENUMBER}" * debug=true; * }; * * ExampleApplication { * com.sun.security.auth.module.LdapLoginModule REQUIRED * userProvider="ldap:///cn=users,dc=example,dc=com" * authIdentity="{USERNAME}" * userFilter="(&(|(samAccountName={USERNAME})(userPrincipalName={USERNAME})(cn={USERNAME}))(objectClass=user))" * useSSL=false * debug=true; * }; * * ExampleApplication { * com.sun.security.auth.module.LdapLoginModule REQUIRED * userProvider="ldap://ldap-svr1 ldap://ldap-svr2" * authIdentity="cn={USERNAME},ou=people,dc=example,dc=com" * authzIdentity="staff" * debug=true; * }; * ** *
* If the application creates a login context using an installed * {@link Configuration} then the application must be granted the * {@link AuthPermission} to create login contexts. * For example, the following security policy allows an application in * the user's current directory to instantiate any login context: *
* * grant codebase "file:${user.dir}/" { * permission javax.security.auth.AuthPermission "createLoginContext.*"; * }; ** * Alternatively, if the application creates a login context using a * caller-specified {@link Configuration} then the application * must be granted the permissions required by the {@link LoginModule}. * This module requires the following two permissions: *
*
* For example, the following security policy grants an application in the * user's current directory all the permissions required by this module: *
* * grant codebase "file:${user.dir}/" { * permission java.net.SocketPermission "*:389", "connect"; * permission java.net.SocketPermission "*:636", "connect"; * permission javax.security.auth.AuthPermission "modifyPrincipals"; * }; **
LoginModule
.
*
* @param subject the Subject
to be authenticated.
* @param callbackHandler a CallbackHandler
to acquire the
* username and password.
* @param sharedState shared LoginModule
state.
* @param options options specified in the login
* Configuration
for this particular
* LoginModule
.
*/
public void initialize(Subject subject, CallbackHandler callbackHandler,
Map Acquire the user's credentials and verify them against the
* specified LDAP directory.
*
* @return true always, since this LoginModule
* should not be ignored.
* @exception FailedLoginException if the authentication fails.
* @exception LoginException if this LoginModule
* is unable to perform the authentication.
*/
public boolean login() throws LoginException {
if (userProvider == null) {
throw new LoginException
("Unable to locate the LDAP directory service");
}
if (debug) {
System.out.println("\t\t[LdapLoginModule] user provider: " +
userProvider);
}
// attempt the authentication
if (tryFirstPass) {
try {
// attempt the authentication by getting the
// username and password from shared state
attemptAuthentication(true);
// authentication succeeded
succeeded = true;
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"tryFirstPass succeeded");
}
return true;
} catch (LoginException le) {
// authentication failed -- try again below by prompting
cleanState();
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"tryFirstPass failed: " + le.toString());
}
}
} else if (useFirstPass) {
try {
// attempt the authentication by getting the
// username and password from shared state
attemptAuthentication(true);
// authentication succeeded
succeeded = true;
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"useFirstPass succeeded");
}
return true;
} catch (LoginException le) {
// authentication failed
cleanState();
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"useFirstPass failed");
}
throw le;
}
}
// attempt the authentication by prompting for the username and pwd
try {
attemptAuthentication(false);
// authentication succeeded
succeeded = true;
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"authentication succeeded");
}
return true;
} catch (LoginException le) {
cleanState();
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"authentication failed");
}
throw le;
}
}
/**
* Complete user authentication.
*
*
This method is called if the LoginContext's * overall authentication succeeded * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules * succeeded). * *
If this LoginModule's own authentication attempt
* succeeded (checked by retrieving the private state saved by the
* This method is called if the overall authentication failed.
* (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
* did not succeed).
*
* If this LoginModule's own authentication attempt
* succeeded (checked by retrieving the private state saved by the
* This method removes the Principals
* that were added by the Also note that this method will set the username and password
* values in the shared state in case subsequent LoginModules
* want to use them via use/tryFirstPass.
*
* @param getPasswdFromSharedState boolean that tells this method whether
* to retrieve the password from the sharedState.
* @exception LoginException if the username/password cannot be acquired.
*/
private void getUsernamePassword(boolean getPasswdFromSharedState)
throws LoginException {
if (getPasswdFromSharedState) {
// use the password saved by the first module in the stack
username = (String)sharedState.get(USERNAME_KEY);
password = (char[])sharedState.get(PASSWORD_KEY);
return;
}
// prompt for a username and password
if (callbackHandler == null)
throw new LoginException("No CallbackHandler available " +
"to acquire authentication information from the user");
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback(rb.getString("username."));
callbacks[1] = new PasswordCallback(rb.getString("password."), false);
try {
callbackHandler.handle(callbacks);
username = ((NameCallback)callbacks[0]).getName();
char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword();
password = new char[tmpPassword.length];
System.arraycopy(tmpPassword, 0,
password, 0, tmpPassword.length);
((PasswordCallback)callbacks[1]).clearPassword();
} catch (java.io.IOException ioe) {
throw new LoginException(ioe.toString());
} catch (UnsupportedCallbackException uce) {
throw new LoginException("Error: " + uce.getCallback().toString() +
" not available to acquire authentication information" +
" from the user");
}
}
/**
* Clean out state because of a failed authentication attempt
*/
private void cleanState() {
username = null;
if (password != null) {
Arrays.fill(password, ' ');
password = null;
}
try {
if (ctx != null) {
ctx.close();
}
} catch (NamingException e) {
// ignore
}
ctx = null;
if (clearPass) {
sharedState.remove(USERNAME_KEY);
sharedState.remove(PASSWORD_KEY);
}
}
}
login
method), then this method associates an
* LdapPrincipal
and one or more UserPrincipal
s
* with the Subject
located in the
* LoginModule
. If this LoginModule's own
* authentication attempted failed, then this method removes
* any state that was originally saved.
*
* @exception LoginException if the commit fails
* @return true if this LoginModule's own login and commit
* attempts succeeded, or false otherwise.
*/
public boolean commit() throws LoginException {
if (succeeded == false) {
return false;
} else {
if (subject.isReadOnly()) {
cleanState();
throw new LoginException ("Subject is read-only");
}
// add Principals to the Subject
Setlogin
and commit
methods),
* then this method cleans up any state that was originally saved.
*
* @exception LoginException if the abort fails.
* @return false if this LoginModule's own login and/or commit attempts
* failed, and true otherwise.
*/
public boolean abort() throws LoginException {
if (debug)
System.out.println("\t\t[LdapLoginModule] " +
"aborted authentication");
if (succeeded == false) {
return false;
} else if (succeeded == true && commitSucceeded == false) {
// Clean out state
succeeded = false;
cleanState();
ldapPrincipal = null;
userPrincipal = null;
authzPrincipal = null;
} else {
// overall authentication succeeded and commit succeeded,
// but someone else's commit failed
logout();
}
return true;
}
/**
* Logout a user.
*
* commit
method.
*
* @exception LoginException if the logout fails.
* @return true in all cases since this LoginModule
* should not be ignored.
*/
public boolean logout() throws LoginException {
if (subject.isReadOnly()) {
cleanState();
throw new LoginException ("Subject is read-only");
}
Set