LdapLoginModulepublic class LdapLoginModule extends Object implements LoginModuleThis {@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
- This option identifies the LDAP directory that stores user entries.
ldap_urls is a list of space-separated LDAP URLs
(RFC 2255)
that identifies the LDAP server to use and the position in
its directory tree where user entries are located.
When several LDAP URLs are specified then each is attempted,
in turn, until the first successful connection is established.
Spaces in the distinguished name component of the URL must be escaped
using the standard mechanism of percent character ('
% ')
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
- This option specifies the search filter to use to locate a user's
entry in the LDAP directory. It is used to determine a user's
distinguished name.
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
- This option specifies the identity to use when authenticating a user
to the LDAP directory.
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
- This option specifies an authorization identity for the user.
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
- if
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
- if
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
- if
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
- if
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
- if
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
- if
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;
};
- Note:
- When a {@link SecurityManager} is active then an application
that creates a {@link LoginContext} and uses a {@link LoginModule}
must be granted certain permissions.
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:
- The {@link SocketPermission} to connect to an LDAP server.
- The {@link AuthPermission} to modify the set of {@link Principal}s
associated with a {@link Subject}.
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";
};
|
Fields Summary |
---|
private static final ResourceBundle | rb | private static final String | USERNAME_KEY | private static final String | PASSWORD_KEY | private static final String | USER_PROVIDER | private static final String | USER_FILTER | private static final String | AUTHC_IDENTITY | private static final String | AUTHZ_IDENTITY | private static final String | USERNAME_TOKEN | private static final Pattern | USERNAME_PATTERN | private String | userProvider | private String | userFilter | private String | authcIdentity | private String | authzIdentity | private String | authzIdentityAttr | private boolean | useSSL | private boolean | authFirst | private boolean | authOnly | private boolean | useFirstPass | private boolean | tryFirstPass | private boolean | storePass | private boolean | clearPass | private boolean | debug | private boolean | succeeded | private boolean | commitSucceeded | private String | username | private char[] | password | private LdapPrincipal | ldapPrincipal | private UserPrincipal | userPrincipal | private UserPrincipal | authzPrincipal | private Subject | subject | private CallbackHandler | callbackHandler | private Map | sharedState | private Map | options | private LdapContext | ctx | private Matcher | identityMatcher | private Matcher | filterMatcher | private Hashtable | ldapEnvironment | private SearchControls | constraints |
Methods Summary |
---|
public boolean | abort()Abort user authentication.
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
login and commit methods),
then this method cleans up any state that was originally saved.
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;
| private void | attemptAuthentication(boolean getPasswdFromSharedState)Attempt authentication
// first get the username and password
getUsernamePassword(getPasswdFromSharedState);
if (password == null || password.length == 0) {
throw (LoginException)
new FailedLoginException("No password was supplied");
}
String dn = "";
if (authFirst || authOnly) {
String id = replaceUsernameToken(identityMatcher, authcIdentity);
// Prepare to bind using user's username and password
ldapEnvironment.put(Context.SECURITY_CREDENTIALS, password);
ldapEnvironment.put(Context.SECURITY_PRINCIPAL, id);
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"attempting to authenticate user: " + username);
}
try {
// Connect to the LDAP server (using simple bind)
ctx = new InitialLdapContext(ldapEnvironment, null);
} catch (NamingException e) {
throw (LoginException)
new FailedLoginException("Cannot bind to LDAP server")
.initCause(e);
}
// Authentication has succeeded
// Locate the user's distinguished name
if (userFilter != null) {
dn = findUserDN(ctx);
} else {
dn = id;
}
} else {
try {
// Connect to the LDAP server (using anonymous bind)
ctx = new InitialLdapContext(ldapEnvironment, null);
} catch (NamingException e) {
throw (LoginException)
new FailedLoginException("Cannot connect to LDAP server")
.initCause(e);
}
// Locate the user's distinguished name
dn = findUserDN(ctx);
try {
// Prepare to bind using user's distinguished name and password
ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"attempting to authenticate user: " + username);
}
// Connect to the LDAP server (using simple bind)
ctx.reconnect(null);
// Authentication has succeeded
} catch (NamingException e) {
throw (LoginException)
new FailedLoginException("Cannot bind to LDAP server")
.initCause(e);
}
}
// Save input as shared state only if authentication succeeded
if (storePass &&
!sharedState.containsKey(USERNAME_KEY) &&
!sharedState.containsKey(PASSWORD_KEY)) {
sharedState.put(USERNAME_KEY, username);
sharedState.put(PASSWORD_KEY, password);
}
// Create the user principals
userPrincipal = new UserPrincipal(username);
if (authzIdentity != null) {
authzPrincipal = new UserPrincipal(authzIdentity);
}
try {
ldapPrincipal = new LdapPrincipal(dn);
} catch (InvalidNameException e) {
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"cannot create LdapPrincipal: bad DN");
}
throw (LoginException)
new FailedLoginException("Cannot create LdapPrincipal")
.initCause(e);
}
| private void | cleanState()Clean out state because of a failed authentication attempt
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);
}
| public boolean | commit()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
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.
if (succeeded == false) {
return false;
} else {
if (subject.isReadOnly()) {
cleanState();
throw new LoginException ("Subject is read-only");
}
// add Principals to the Subject
Set principals = subject.getPrincipals();
if (! principals.contains(ldapPrincipal)) {
principals.add(ldapPrincipal);
}
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"added LdapPrincipal \"" +
ldapPrincipal +
"\" to Subject");
}
if (! principals.contains(userPrincipal)) {
principals.add(userPrincipal);
}
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"added UserPrincipal \"" +
userPrincipal +
"\" to Subject");
}
if (authzPrincipal != null &&
(! principals.contains(authzPrincipal))) {
principals.add(authzPrincipal);
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"added UserPrincipal \"" +
authzPrincipal +
"\" to Subject");
}
}
}
// in any case, clean out state
cleanState();
commitSucceeded = true;
return true;
| private java.lang.String | findUserDN(javax.naming.ldap.LdapContext ctx)Search for the user's entry.
Determine the distinguished name of the user's entry and optionally
an authorization identity for the user.
String userDN = "";
// Locate the user's LDAP entry
if (userFilter != null) {
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"searching for entry belonging to user: " + username);
}
} else {
if (debug) {
System.out.println("\t\t[LdapLoginModule] " +
"cannot search for entry belonging to user: " + username);
}
throw (LoginException)
new FailedLoginException("Cannot find user's LDAP entry");
}
try {
NamingEnumeration results = ctx.search("",
replaceUsernameToken(filterMatcher, userFilter), constraints);
// Extract the distinguished name of the user's entry
// (Use the first entry if more than one is returned)
if (results.hasMore()) {
SearchResult entry = (SearchResult) results.next();
// %%% - use the SearchResult.getNameInNamespace method
// available in JDK 1.5 and later.
// (can remove call to constraints.setReturningObjFlag)
userDN = ((Context)entry.getObject()).getNameInNamespace();
if (debug) {
System.out.println("\t\t[LdapLoginModule] found entry: " +
userDN);
}
// Extract a value from user's authorization identity attribute
if (authzIdentityAttr != null) {
Attribute attr =
entry.getAttributes().get(authzIdentityAttr);
if (attr != null) {
Object val = attr.get();
if (val instanceof String) {
authzIdentity = (String) val;
}
}
}
results.close();
} else {
// Bad username
if (debug) {
System.out.println("\t\t[LdapLoginModule] user's entry " +
"not found");
}
}
} catch (NamingException e) {
// ignore
}
if (userDN.equals("")) {
throw (LoginException)
new FailedLoginException("Cannot find user's LDAP entry");
} else {
return userDN;
}
| private void | getUsernamePassword(boolean getPasswdFromSharedState)Get the username and password.
This method does not return any value.
Instead, it sets global name and password variables.
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.
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");
}
| public void | initialize(javax.security.auth.Subject subject, javax.security.auth.callback.CallbackHandler callbackHandler, java.util.Map sharedState, java.util.Map options)Initialize this LoginModule .
this.subject = subject;
this.callbackHandler = callbackHandler;
this.sharedState = sharedState;
this.options = options;
ldapEnvironment = new Hashtable(9);
ldapEnvironment.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
// Add any JNDI properties to the environment
Set keys = options.keySet();
String key;
for (Iterator i = keys.iterator(); i.hasNext(); ) {
key = (String) i.next();
if (key.indexOf(".") > -1) {
ldapEnvironment.put(key, options.get(key));
}
}
// initialize any configured options
userProvider = (String)options.get(USER_PROVIDER);
if (userProvider != null) {
ldapEnvironment.put(Context.PROVIDER_URL, userProvider);
}
authcIdentity = (String)options.get(AUTHC_IDENTITY);
if (authcIdentity != null &&
(authcIdentity.indexOf(USERNAME_TOKEN) != -1)) {
identityMatcher = USERNAME_PATTERN.matcher(authcIdentity);
}
userFilter = (String)options.get(USER_FILTER);
if (userFilter != null) {
if (userFilter.indexOf(USERNAME_TOKEN) != -1) {
filterMatcher = USERNAME_PATTERN.matcher(userFilter);
}
constraints = new SearchControls();
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
constraints.setReturningAttributes(new String[0]); //return no attrs
constraints.setReturningObjFlag(true); // to get the full DN
}
authzIdentity = (String)options.get(AUTHZ_IDENTITY);
if (authzIdentity != null &&
authzIdentity.startsWith("{") && authzIdentity.endsWith("}")) {
if (constraints != null) {
authzIdentityAttr =
authzIdentity.substring(1, authzIdentity.length() - 1);
constraints.setReturningAttributes(
new String[]{authzIdentityAttr});
}
authzIdentity = null; // set later, from the specified attribute
}
// determine mode
if (authcIdentity != null) {
if (userFilter != null) {
authFirst = true; // authentication-first mode
} else {
authOnly = true; // authentication-only mode
}
}
if ("false".equalsIgnoreCase((String)options.get("useSSL"))) {
useSSL = false;
ldapEnvironment.remove(Context.SECURITY_PROTOCOL);
} else {
ldapEnvironment.put(Context.SECURITY_PROTOCOL, "ssl");
}
tryFirstPass =
"true".equalsIgnoreCase((String)options.get("tryFirstPass"));
useFirstPass =
"true".equalsIgnoreCase((String)options.get("useFirstPass"));
storePass = "true".equalsIgnoreCase((String)options.get("storePass"));
clearPass = "true".equalsIgnoreCase((String)options.get("clearPass"));
debug = "true".equalsIgnoreCase((String)options.get("debug"));
if (debug) {
if (authFirst) {
System.out.println("\t\t[LdapLoginModule] " +
"authentication-first mode; " +
(useSSL ? "SSL enabled" : "SSL disabled"));
} else if (authOnly) {
System.out.println("\t\t[LdapLoginModule] " +
"authentication-only mode; " +
(useSSL ? "SSL enabled" : "SSL disabled"));
} else {
System.out.println("\t\t[LdapLoginModule] " +
"search-first mode; " +
(useSSL ? "SSL enabled" : "SSL disabled"));
}
}
| public boolean | login()Begin user authentication.
Acquire the user's credentials and verify them against the
specified LDAP directory.
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;
}
| public boolean | logout()Logout a user.
This method removes the Principals
that were added by the commit method.
if (subject.isReadOnly()) {
cleanState();
throw new LoginException ("Subject is read-only");
}
Set principals = subject.getPrincipals();
principals.remove(ldapPrincipal);
principals.remove(userPrincipal);
if (authzIdentity != null) {
principals.remove(authzPrincipal);
}
// clean out state
cleanState();
succeeded = false;
commitSucceeded = false;
ldapPrincipal = null;
userPrincipal = null;
authzPrincipal = null;
if (debug) {
System.out.println("\t\t[LdapLoginModule] logged out Subject");
}
return true;
| private java.lang.String | replaceUsernameToken(java.util.regex.Matcher matcher, java.lang.String string)Replace the username token
return matcher != null ? matcher.replaceAll(username) : string;
|
|