/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.jaas;
import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides functionality common to ldap based JAAS login modules.
*
* @author Middleware Services
*/
public abstract class AbstractLoginModule implements LoginModule
{
/** Constant for login name stored in shared state. */
public static final String LOGIN_NAME = "javax.security.auth.login.name";
/** Constant for entryDn stored in shared state. */
public static final String LOGIN_DN = "org.ldaptive.jaas.login.entryDn";
/** Constant for login password stored in shared state. */
public static final String LOGIN_PASSWORD = "javax.security.auth.login.password";
/** Logger for this class. */
protected final Logger logger = LoggerFactory.getLogger(getClass());
/** Default roles. */
protected final List<LdapRole> defaultRole = new ArrayList<>();
/** Initialized subject. */
protected Subject subject;
/** Initialized callback handler. */
protected CallbackHandler callbackHandler;
/** Shared state from other login module. */
protected Map sharedState;
/** Whether credentials from the shared state should be used. */
protected boolean useFirstPass;
/** Whether credentials from the shared state should be used if they are available. */
protected boolean tryFirstPass;
/** Whether credentials should be stored in the shared state map. */
protected boolean storePass;
/** Whether credentials should be removed from the shared state map. */
protected boolean clearPass;
/** Whether ldap principal data should be set. */
protected boolean setLdapPrincipal;
/** Whether ldap dn principal data should be set. */
protected boolean setLdapDnPrincipal;
/** Whether ldap credential data should be set. */
protected boolean setLdapCredential;
/** Name of group to add all principals to. */
protected String principalGroupName;
/** Name of group to add all roles to. */
protected String roleGroupName;
/** Whether authentication was successful. */
protected boolean loginSuccess;
/** Whether commit was successful. */
protected boolean commitSuccess;
/** Principals to add to the subject. */
protected Set<Principal> principals;
/** Credentials to add to the subject. */
protected Set<LdapCredential> credentials;
/** Roles to add to the subject. */
protected Set<Principal> roles;
@Override
public void initialize(
final Subject subj,
final CallbackHandler handler,
final Map<String, ?> state,
final Map<String, ?> options)
{
logger.trace("Begin initialize");
subject = subj;
callbackHandler = handler;
sharedState = state;
for (String key : options.keySet()) {
final String value = (String) options.get(key);
if ("useFirstPass".equalsIgnoreCase(key)) {
useFirstPass = Boolean.valueOf(value);
} else if ("tryFirstPass".equalsIgnoreCase(key)) {
tryFirstPass = Boolean.valueOf(value);
} else if ("storePass".equalsIgnoreCase(key)) {
storePass = Boolean.valueOf(value);
} else if ("clearPass".equalsIgnoreCase(key)) {
clearPass = Boolean.valueOf(value);
} else if ("setLdapPrincipal".equalsIgnoreCase(key)) {
setLdapPrincipal = Boolean.valueOf(value);
} else if ("setLdapDnPrincipal".equalsIgnoreCase(key)) {
setLdapDnPrincipal = Boolean.valueOf(value);
} else if ("setLdapCredential".equalsIgnoreCase(key)) {
setLdapCredential = Boolean.valueOf(value);
} else if ("defaultRole".equalsIgnoreCase(key)) {
for (String s : value.split(",")) {
defaultRole.add(new LdapRole(s.trim()));
}
} else if ("principalGroupName".equalsIgnoreCase(key)) {
principalGroupName = value;
} else if ("roleGroupName".equalsIgnoreCase(key)) {
roleGroupName = value;
}
}
logger.trace(
"useFirstPass = {}, tryFirstPass = {}, storePass = {}, clearPass = {}, " +
"setLdapPrincipal = {}, setLdapDnPrincipal = {}, " +
"setLdapCredential = {}, defaultRole = {}, principalGroupName = {}, " +
"roleGroupName = {}",
Boolean.toString(useFirstPass),
Boolean.toString(tryFirstPass),
Boolean.toString(storePass),
Boolean.toString(clearPass),
Boolean.toString(setLdapPrincipal),
Boolean.toString(setLdapDnPrincipal),
Boolean.toString(setLdapCredential),
defaultRole,
principalGroupName,
roleGroupName);
principals = new TreeSet<>();
credentials = new HashSet<>();
roles = new TreeSet<>();
}
@Override
public boolean login()
throws LoginException
{
final NameCallback nameCb = new NameCallback("Enter user: ");
final PasswordCallback passCb = new PasswordCallback("Enter user password: ", false);
return login(nameCb, passCb);
}
/**
* Authenticates a {@link Subject} with the supplied callbacks.
*
* @param nameCb callback handler for subject's name
* @param passCb callback handler for subject's password
*
* @return true if authentication succeeded, false to ignore this module
*
* @throws LoginException if the authentication fails
*/
protected abstract boolean login(final NameCallback nameCb, final PasswordCallback passCb)
throws LoginException;
@Override
public boolean commit()
throws LoginException
{
logger.trace("Begin commit");
if (!loginSuccess) {
logger.debug("Login failed");
return false;
}
if (subject.isReadOnly()) {
clearState();
throw new LoginException("Subject is read-only.");
}
subject.getPrincipals().addAll(principals);
logger.debug("Committed the following principals: {}", principals);
subject.getPrivateCredentials().addAll(credentials);
subject.getPrincipals().addAll(roles);
logger.debug("Committed the following roles: {}", roles);
if (principalGroupName != null) {
final LdapGroup group = new LdapGroup(principalGroupName);
principals.forEach(group::addMember);
subject.getPrincipals().add(group);
logger.debug("Committed the following principal group: {}", group);
}
if (roleGroupName != null) {
final LdapGroup group = new LdapGroup(roleGroupName);
roles.forEach(group::addMember);
subject.getPrincipals().add(group);
logger.debug("Committed the following role group: {}", group);
}
clearState();
commitSuccess = true;
return true;
}
@Override
public boolean abort()
throws LoginException
{
logger.trace("Begin abort");
if (!loginSuccess) {
return false;
} else if (!commitSuccess) {
loginSuccess = false;
clearState();
} else {
logout();
}
return true;
}
@Override
public boolean logout()
throws LoginException
{
logger.trace("Begin logout");
if (subject.isReadOnly()) {
clearState();
throw new LoginException("Subject is read-only.");
}
for (LdapPrincipal ldapPrincipal : subject.getPrincipals(LdapPrincipal.class)) {
subject.getPrincipals().remove(ldapPrincipal);
}
for (LdapDnPrincipal ldapDnPrincipal : subject.getPrincipals(LdapDnPrincipal.class)) {
subject.getPrincipals().remove(ldapDnPrincipal);
}
for (LdapRole ldapRole : subject.getPrincipals(LdapRole.class)) {
subject.getPrincipals().remove(ldapRole);
}
for (LdapGroup ldapGroup : subject.getPrincipals(LdapGroup.class)) {
subject.getPrincipals().remove(ldapGroup);
}
for (LdapCredential ldapCredential : subject.getPrivateCredentials(LdapCredential.class)) {
subject.getPrivateCredentials().remove(ldapCredential);
}
clearState();
loginSuccess = false;
commitSuccess = false;
return true;
}
/**
* Removes any stateful principals, credentials, or roles stored by login. Also removes shared state name, dn, and
* password if clearPass is set.
*/
protected void clearState()
{
principals.clear();
credentials.clear();
roles.clear();
if (clearPass) {
sharedState.remove(LOGIN_NAME);
sharedState.remove(LOGIN_PASSWORD);
sharedState.remove(LOGIN_DN);
}
}
/**
* Attempts to retrieve credentials for the supplied name and password callbacks. If useFirstPass or tryFirstPass is
* set, then name and password data is retrieved from shared state. Otherwise a callback handler is used to get the
* data. Set useCallback to force a callback handler to be used.
*
* @param nameCb to set name for
* @param passCb to set password for
* @param useCallback whether to force a callback handler
*
* @throws LoginException if the callback handler fails
*/
protected void getCredentials(final NameCallback nameCb, final PasswordCallback passCb, final boolean useCallback)
throws LoginException
{
logger.trace(
"Begin getCredentials: useFistPass = {}, tryFistPass = {}, " +
"useCallback = {}, callbackhandler class = {}, " +
"name callback class = {}, password callback class = {}",
Boolean.toString(useFirstPass),
Boolean.toString(tryFirstPass),
Boolean.toString(useCallback),
callbackHandler.getClass().getName(),
nameCb.getClass().getName(),
passCb.getClass().getName());
try {
if ((useFirstPass || tryFirstPass) && !useCallback) {
nameCb.setName((String) sharedState.get(LOGIN_NAME));
passCb.setPassword((char[]) sharedState.get(LOGIN_PASSWORD));
} else if (callbackHandler != null) {
callbackHandler.handle(new Callback[] {nameCb, passCb});
} else {
throw new LoginException(
"No CallbackHandler available. " +
"Set useFirstPass, tryFirstPass, or provide a CallbackHandler");
}
} catch (IOException e) {
logger.error("Error reading data from callback handler", e);
loginSuccess = false;
throw new LoginException(e.getMessage());
} catch (UnsupportedCallbackException e) {
logger.error("Unsupported callback", e);
loginSuccess = false;
throw new LoginException(e.getMessage());
}
}
/**
* Stores the supplied name, password, and entry dn in the stored state map. storePass must be set for this method to
* have any affect.
*
* @param nameCb to store
* @param passCb to store
* @param loginDn to store
*/
@SuppressWarnings("unchecked")
protected void storeCredentials(final NameCallback nameCb, final PasswordCallback passCb, final String loginDn)
{
if (storePass) {
if (nameCb != null && nameCb.getName() != null) {
sharedState.put(LOGIN_NAME, nameCb.getName());
}
if (passCb != null && passCb.getPassword() != null) {
sharedState.put(LOGIN_PASSWORD, passCb.getPassword());
}
if (loginDn != null) {
sharedState.put(LOGIN_DN, loginDn);
}
}
}
}