/* * @(#)JndiLoginModule.java 1.11 04/05/05 * * Copyright 2004 Sun Microsystems, Inc. All rights reserved. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. */ package com.sun.security.auth.module; import javax.security.auth.*; import javax.security.auth.callback.*; import javax.security.auth.login.*; import javax.security.auth.spi.*; import javax.naming.*; import javax.naming.directory.*; import java.io.IOException; import java.util.Map; import java.util.LinkedList; import java.util.ResourceBundle; import com.sun.security.auth.UnixPrincipal; import com.sun.security.auth.UnixNumericUserPrincipal; import com.sun.security.auth.UnixNumericGroupPrincipal; import sun.security.util.AuthResources; /** *
The module prompts for a username and password * and then verifies the password against the password stored in * a directory service configured under JNDI. * *
This LoginModule
interoperates with
* any conformant JNDI service provider. To direct this
* LoginModule
to use a specific JNDI service provider,
* two options must be specified in the login Configuration
* for this LoginModule
.
*
* user.provider.url=name_service_url * group.provider.url=name_service_url ** * name_service_url specifies * the directory service and path where this
LoginModule
* can access the relevant user and group information. Because this
* LoginModule
only performs one-level searches to
* find the relevant user information, the URL
* must point to a directory one level above where the user and group
* information is stored in the directory service.
* For example, to instruct this LoginModule
* to contact a NIS server, the following URLs must be specified:
* * user.provider.url="nis://NISServerHostName/NISDomain/user" * group.provider.url="nis://NISServerHostName/NISDomain/system/group" ** * NISServerHostName specifies the server host name of the * NIS server (for example, nis.sun.com, and NISDomain * specifies the domain for that NIS server (for example, jaas.sun.com. * To contact an LDAP server, the following URLs must be specified: *
* user.provider.url="ldap://LDAPServerHostName/LDAPName" * group.provider.url="ldap://LDAPServerHostName/LDAPName" ** * LDAPServerHostName specifies the server host name of the * LDAP server, which may include a port number * (for example, ldap.sun.com:389), * and LDAPName specifies the entry name in the LDAP directory * (for example, ou=People,o=Sun,c=US and ou=Groups,o=Sun,c=US * for user and group information, respectively). * *
The format in which the user's information must be stored in
* the directory service is specified in RFC 2307. Specifically,
* this LoginModule
will search for the user's entry in the
* directory service using the user's uid attribute,
* where uid=username. If the search succeeds,
* this LoginModule
will then
* obtain the user's encrypted password from the retrieved entry
* using the userPassword attribute.
* This LoginModule
assumes that the password is stored
* as a byte array, which when converted to a String
,
* has the following format:
*
* "{crypt}encrypted_password" ** * The LDAP directory server must be configured * to permit read access to the userPassword attribute. * If the user entered a valid username and password, * this
LoginModule
associates a
* UnixPrincipal
, UnixNumericUserPrincipal
,
* and the relevant UnixNumericGroupPrincipals with the
* Subject
.
*
* This LoginModule also recognizes the following Configuration
* options:
*
* debug if, true, debug messages are output to System.out.
*
* useFirstPass if, true, this LoginModule 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 LoginModule retrieves the
* 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
* 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 LoginModule stores the username and password
* obtained from the 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 LoginModule
clears the
* username and password stored in the module's shared state
* after both phases of authentication (login and commit)
* have completed.
*
*
* @version 1.11, 05/05/04
*/
public class JndiLoginModule implements LoginModule {
static final java.util.ResourceBundle rb =
java.util.ResourceBundle.getBundle("sun.security.util.AuthResources");
/** JNDI Provider */
public final String USER_PROVIDER = "user.provider.url";
public final String GROUP_PROVIDER = "group.provider.url";
// configurable options
private boolean debug = false;
private boolean strongDebug = false;
private String userProvider;
private String groupProvider;
private boolean useFirstPass = false;
private boolean tryFirstPass = false;
private boolean storePass = false;
private boolean clearPass = false;
// the authentication status
private boolean succeeded = false;
private boolean commitSucceeded = false;
// username, password, and JNDI context
private String username;
private char[] password;
DirContext ctx;
// the user (assume it is a UnixPrincipal)
private UnixPrincipal userPrincipal;
private UnixNumericUserPrincipal UIDPrincipal;
private UnixNumericGroupPrincipal GIDPrincipal;
private LinkedList supplementaryGroups = new LinkedList();
// initial state
private Subject subject;
private CallbackHandler callbackHandler;
private Map sharedState;
private Map options;
private static final String CRYPT = "{crypt}";
private static final String USER_PWD = "userPassword";
private static final String USER_UID = "uidNumber";
private static final String USER_GID = "gidNumber";
private static final String GROUP_ID = "gidNumber";
private static final String NAME = "javax.security.auth.login.name";
private static final String PWD = "javax.security.auth.login.password";
/**
* Initialize this LoginModule
.
*
*
*
* @param subject the Subject
to be authenticated.
*
* @param callbackHandler a CallbackHandler
for communicating
* with the end user (prompting for usernames and
* passwords, for example).
*
* @param sharedState shared LoginModule
state.
*
* @param options options specified in the login
* Prompt for username and password.
* Verify the password against the relevant name service.
*
*
*
* @return true always, since this
*
* @exception LoginException if this 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
*
*
* @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 Readonly");
}
// add Principals to the Subject
if (!subject.getPrincipals().contains(userPrincipal))
subject.getPrincipals().add(userPrincipal);
if (!subject.getPrincipals().contains(UIDPrincipal))
subject.getPrincipals().add(UIDPrincipal);
if (!subject.getPrincipals().contains(GIDPrincipal))
subject.getPrincipals().add(GIDPrincipal);
for (int i = 0; i < supplementaryGroups.size(); i++) {
if (!subject.getPrincipals().contains
((UnixNumericGroupPrincipal)supplementaryGroups.get(i)))
subject.getPrincipals().add((UnixNumericGroupPrincipal)
supplementaryGroups.get(i));
}
if (debug) {
System.out.println("\t\t[JndiLoginModule]: " +
"added UnixPrincipal,");
System.out.println("\t\t\t\tUnixNumericUserPrincipal,");
System.out.println("\t\t\t\tUnixNumericGroupPrincipal(s),");
System.out.println("\t\t\t to Subject");
}
}
// in any case, clean out state
cleanState();
commitSucceeded = true;
return true;
}
/**
* This method is called if the LoginContext's
* 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
*
*
* @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[JndiLoginModule]: " +
"aborted authentication failed");
if (succeeded == false) {
return false;
} else if (succeeded == true && commitSucceeded == false) {
// Clean out state
succeeded = false;
cleanState();
userPrincipal = null;
UIDPrincipal = null;
GIDPrincipal = null;
supplementaryGroups = new LinkedList();
} else {
// overall authentication succeeded and commit succeeded,
// but someone else's commit failed
logout();
}
return true;
}
/**
* Logout a user.
*
* This method removes the Principals
* that were added by the
*
* @exception LoginException if the logout fails.
*
* @return true in all cases since this
*
* @param getPasswdFromSharedState boolean that tells this method whether
* to retrieve the password from the sharedState.
*/
private void attemptAuthentication(boolean getPasswdFromSharedState)
throws LoginException {
String encryptedPassword = null;
// first get the username and password
getUsernamePassword(getPasswdFromSharedState);
try {
// get the user's passwd entry from the user provider URL
InitialContext iCtx = new InitialContext();
ctx = (DirContext)iCtx.lookup(userProvider);
/*
SearchControls controls = new SearchControls
(SearchControls.ONELEVEL_SCOPE,
0,
5000,
new String[] { USER_PWD },
false,
false);
*/
SearchControls controls = new SearchControls();
NamingEnumeration ne = ctx.search("",
"(uid=" + username + ")",
controls);
if (ne.hasMore()) {
SearchResult result = (SearchResult)ne.next();
Attributes attributes = result.getAttributes();
// get the password
// this module works only if the LDAP directory server
// is configured to permit read access to the userPassword
// attribute. The directory administrator need to grant
// this access.
//
// A workaround would be to make the server do authentication
// by setting the Context.SECURITY_PRINCIPAL
// and Context.SECURITY_CREDENTIALS property.
// However, this would make it not work with systems that
// don't do authentication at the server (like NIS).
//
// Setting the SECURITY_* properties and using "simple"
// authentication for LDAP is recommended only for secure
// channels. For nonsecure channels, SSL is recommended.
Attribute pwd = attributes.get(USER_PWD);
String encryptedPwd = new String((byte[])pwd.get(), "UTF8");
encryptedPassword = encryptedPwd.substring(CRYPT.length());
// check the password
if (verifyPassword
(encryptedPassword, new String(password)) == true) {
// authentication succeeded
if (debug)
System.out.println("\t\t[JndiLoginModule] " +
"attemptAuthentication() succeeded");
} else {
// authentication failed
if (debug)
System.out.println("\t\t[JndiLoginModule] " +
"attemptAuthentication() failed");
throw new FailedLoginException("Login incorrect");
}
// save input as shared state only if
// authentication succeeded
if (storePass &&
!sharedState.containsKey(NAME) &&
!sharedState.containsKey(PWD)) {
sharedState.put(NAME, username);
sharedState.put(PWD, password);
}
// create the user principal
userPrincipal = new UnixPrincipal(username);
// get the UID
Attribute uid = attributes.get(USER_UID);
String uidNumber = (String)uid.get();
UIDPrincipal = new UnixNumericUserPrincipal(uidNumber);
if (debug && uidNumber != null) {
System.out.println("\t\t[JndiLoginModule] " +
"user: '" + username + "' has UID: " +
uidNumber);
}
// get the GID
Attribute gid = attributes.get(USER_GID);
String gidNumber = (String)gid.get();
GIDPrincipal = new UnixNumericGroupPrincipal
(gidNumber, true);
if (debug && gidNumber != null) {
System.out.println("\t\t[JndiLoginModule] " +
"user: '" + username + "' has GID: " +
gidNumber);
}
// get the supplementary groups from the group provider URL
ctx = (DirContext)iCtx.lookup(groupProvider);
ne = ctx.search("", new BasicAttributes("memberUid", username));
while (ne.hasMore()) {
result = (SearchResult)ne.next();
attributes = result.getAttributes();
gid = attributes.get(GROUP_ID);
String suppGid = (String)gid.get();
if (!gidNumber.equals(suppGid)) {
UnixNumericGroupPrincipal suppPrincipal =
new UnixNumericGroupPrincipal(suppGid, false);
supplementaryGroups.add(suppPrincipal);
if (debug && suppGid != null) {
System.out.println("\t\t[JndiLoginModule] " +
"user: '" + username +
"' has Supplementary Group: " +
suppGid);
}
}
}
} else {
// bad username
if (debug) {
System.out.println("\t\t[JndiLoginModule]: User not found");
}
throw new FailedLoginException("User not found");
}
} catch (NamingException ne) {
// bad username
if (debug) {
System.out.println("\t\t[JndiLoginModule]: User not found");
ne.printStackTrace();
}
throw new FailedLoginException("User not found");
} catch (java.io.UnsupportedEncodingException uee) {
// password stored in incorrect format
if (debug) {
System.out.println("\t\t[JndiLoginModule]: " +
"password incorrectly encoded");
uee.printStackTrace();
}
throw new LoginException("Login failure due to incorrect " +
"password encoding in the password database");
}
// authentication succeeded
}
/**
* 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.
*
*
*
* @param getPasswdFromSharedState boolean that tells this method whether
* to retrieve the password from the sharedState.
*/
private void getUsernamePassword(boolean getPasswdFromSharedState)
throws LoginException {
if (getPasswdFromSharedState) {
// use the password saved by the first module in the stack
username = (String)sharedState.get(NAME);
password = (char[])sharedState.get(PWD);
return;
}
// prompt for a username and password
if (callbackHandler == null)
throw new LoginException("Error: no CallbackHandler available " +
"to garner authentication information from the user");
String protocol = userProvider.substring(0, userProvider.indexOf(":"));
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback(protocol + " "
+ rb.getString("username: "));
callbacks[1] = new PasswordCallback(protocol + " " +
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 garner authentication information " +
"from the user");
}
// print debugging information
if (strongDebug) {
System.out.println("\t\t[JndiLoginModule] " +
"user entered username: " +
username);
System.out.print("\t\t[JndiLoginModule] " +
"user entered password: ");
for (int i = 0; i < password.length; i++)
System.out.print(password[i]);
System.out.println();
}
}
/**
* Verify a password against the encrypted passwd from /etc/shadow
*/
private boolean verifyPassword(String encryptedPassword, String password) {
if (encryptedPassword == null)
return false;
Crypt c = new Crypt();
try {
byte oldCrypt[] = encryptedPassword.getBytes("UTF8");
byte newCrypt[] = c.crypt(password.getBytes("UTF8"),
oldCrypt);
if (newCrypt.length != oldCrypt.length)
return false;
for (int i = 0; i < newCrypt.length; i++) {
if (oldCrypt[i] != newCrypt[i])
return false;
}
} catch (java.io.UnsupportedEncodingException uee) {
// cannot happen, but return false just to be safe
return false;
}
return true;
}
/**
* Clean out state because of a failed authentication attempt
*/
private void cleanState() {
username = null;
if (password != null) {
for (int i = 0; i < password.length; i++)
password[i] = ' ';
password = null;
}
ctx = null;
if (clearPass) {
sharedState.remove(NAME);
sharedState.remove(PWD);
}
}
}
Configuration
for this particular
* LoginModule
.
*/
public void initialize(Subject subject, CallbackHandler callbackHandler,
MapLoginModule
* should not be ignored.
*
* @exception FailedLoginException if the authentication fails. LoginModule
* is unable to perform the authentication.
*/
public boolean login() throws LoginException {
if (userProvider == null) {
throw new LoginException
("Error: Unable to locate JNDI user provider");
}
if (groupProvider == null) {
throw new LoginException
("Error: Unable to locate JNDI group provider");
}
if (debug) {
System.out.println("\t\t[JndiLoginModule] user provider: " +
userProvider);
System.out.println("\t\t[JndiLoginModule] group provider: " +
groupProvider);
}
// 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[JndiLoginModule] " +
"tryFirstPass succeeded");
}
return true;
} catch (LoginException le) {
// authentication failed -- try again below by prompting
cleanState();
if (debug) {
System.out.println("\t\t[JndiLoginModule] " +
"tryFirstPass failed with:" +
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[JndiLoginModule] " +
"useFirstPass succeeded");
}
return true;
} catch (LoginException le) {
// authentication failed
cleanState();
if (debug) {
System.out.println("\t\t[JndiLoginModule] " +
"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[JndiLoginModule] " +
"regular authentication succeeded");
}
return true;
} catch (LoginException le) {
cleanState();
if (debug) {
System.out.println("\t\t[JndiLoginModule] " +
"regular authentication failed");
}
throw le;
}
}
/**
* Abstract method to commit the authentication process (phase 2).
*
* login
method), then this method associates a
* UnixPrincipal
* 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.
*
* login
and commit
methods),
* then this method cleans up any state that was originally saved.
*
* commit
method.
*
* LoginModule
* should not be ignored.
*/
public boolean logout() throws LoginException {
if (subject.isReadOnly()) {
cleanState();
throw new LoginException ("Subject is Readonly");
}
subject.getPrincipals().remove(userPrincipal);
subject.getPrincipals().remove(UIDPrincipal);
subject.getPrincipals().remove(GIDPrincipal);
for (int i = 0; i < supplementaryGroups.size(); i++) {
subject.getPrincipals().remove
((UnixNumericGroupPrincipal)supplementaryGroups.get(i));
}
// clean out state
cleanState();
succeeded = false;
commitSucceeded = false;
userPrincipal = null;
UIDPrincipal = null;
GIDPrincipal = null;
supplementaryGroups = new LinkedList();
if (debug) {
System.out.println("\t\t[JndiLoginModule]: " +
"logged out Subject");
}
return true;
}
/**
* Attempt authentication
*
*