[OSUSER-7] LDAP credential provider - solution Created: 22/Dec/04  Updated: 22/Dec/04

Status: Open
Project: osuser
Component/s: www
Affects Version/s: current
Fix Version/s: milestone 1

Type: Task Priority: Major
Reporter: amichalec Assignee: osuser-issues
Resolution: Unresolved Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified
Environment:

Operating System: All
Platform: All


Issuezilla Id: 7

 Description   

Damn this issue tracker (or its config). I cannot neither add attachment nor
edit/comment... Let any admin cleans it up if necessary.

Here is a solution for https://osuser.dev.java.net/issues/show_bug.cgi?id=6 in a
form of listing.

andrew michalec.

=========================

/*

  • Copyright (c) 2002-2003 by OpenSymphony
  • All rights reserved.
    */
    package com.opensymphony.user.provider.ldap;

import com.opensymphony.user.Entity;
import com.opensymphony.user.UserManager;
import com.opensymphony.user.provider.CredentialsProvider;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.*;

import javax.naming.AuthenticationException;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;

/**

  • Provider for checking credentials against a LDAP directory.
  • <p>
  • Tries to connect to an LDAP directory with the specified username/password.
    Succeeds or fails depending on whether the
  • LDAP authentication succeeds/fails.
  • <p>
  • The authentication algorithm is as follows:
  • <ul>
  • <li>Establish an anonymous or authenticated connection with LDAP
  • <li>Search within a subtree for a node representing the user, eg. search
    below 'ou=People,dc=example,dc=com' for (uid=fred) if the username is 'fred'.
  • <li>If a user node is found (eg. 'uid=fred,ou=People,dc=example,dc=com'),
    try to connect to LDAP again, using the
  • found node as the 'bind DN', and using the user's password.
  • <li>If this second connection succeeds, return true
  • </ul>
  • <p>A sample osuser.xml configuration:
  • <pre>
  • <provider class="com.opensymphony.user.provider.ldap.LDAPCredentialsProvider">
    <property
    name="java.naming.factory.initial">com.sun.jndi.ldap.LdapCtxFactory</property>
    <property name="java.naming.provider.url">ldap://localhost:389</property>
    <property name="searchBase">ou=People,dc=example,dc=com</property>
    <property name="uidSearchName">uid</property>
    <property
    name="java.naming.security.principal">cn=admin,dc=example,dc=com</property>
    <property name="java.naming.security.credentials">secret</property>
    <property name="exclusive-access">true</property>
    </provider>
  • </pre>
  • The security principal and credentials lines are optional, depending on
    whether your initial connection need be authenticated or not.
  • <p>
  • Currently, there must be <strong>at least one other non-LDAP
    CredentialsProvider</strong> configured for this provider to work.
  • This is because there are user management features that
    LDAPCredentialsProvider does not provide on its own (create/delete user, change
    password).
  • When calls to these methods are made, LDAPCredentialsProvider delegates the
    call to the other CredentialsProvider implementation.
  • <p>
  • Notes:
  • <ul>
  • <li>Entering blank password will always fail, regardless of whether the
    underlying LDAP allows anonymous user connects.
  • <li>If the initial LDAP connection cannot be established, or there is an
    unexpected error, the authentication attempt
  • is passed on to other non-LDAP CredentialsProvider.
  • <li>If the user exists in LDAP but the password was incorrect, the module
    fails without consulting other CredentialsProviders.
  • <li>Turning logging up to DEBUG will reveal details on authentication attempts.
  • </ul>
    *
  • @author <a href="mailto:jeff@atlassian.com">Jeff Turner</a>
    */
    public class LDAPCredentialsProvider implements CredentialsProvider {
    //~ Static fields/initializers /////////////////////////////////////////////

private static final Log log = LogFactory.getLog(LDAPCredentialsProvider.class);
static private HashMap cache = new HashMap();

//~ Instance fields ////////////////////////////////////////////////////////

private Hashtable env;
private String searchBase;
private String uidSearchName;
private long timeout;

//~ Methods ////////////////////////////////////////////////////////////////

public boolean authenticate(String name, String password) {
// Do NOT allow null or empty passwords
// This is required as LDAP (by default) allows an empty password for an
existing user
// (if the password is not specified LDAP allows the user to connect,
and treats the user as
// an unauthenticated - anonymous user)
if ((password == null) || "".equals(password))

{ return false; }

// check cache
TimeAndPassword tp = (TimeAndPassword) cache.get(name);

if ((tp != null) && tp.password.equals(password) && (tp.time >
System.currentTimeMillis())) {
if (log.isDebugEnabled())

{ log.debug("Successful authentication for " + name + " from cached LDAP lookup"); }

return true;
}

DirContext ctx = null;

try

{ ctx = new InitialDirContext(env); }

catch (NamingException e)

{ log.error("Could not connect to LDAP. Please check your " + "host ('" + env.get(Context.PROVIDER_URL) + "'), " + "bind DN ('" + env.get(Context.SECURITY_PRINCIPAL) + "') and bind password.", e); return tryOtherCredentialsProviders(name, password); }

StringBuffer filterBuffer = new
StringBuffer(uidSearchName).append("=").append(name);

String[] attrIDs =

{uidSearchName}

;
SearchControls ctls = new SearchControls();
ctls.setReturningAttributes(attrIDs);
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);

if (log.isDebugEnabled())

{ log.debug("Doing initial search: " + "username='" + env.get(Context.SECURITY_PRINCIPAL) + "', password='" + env.get(Context.SECURITY_CREDENTIALS) + "', base='" + searchBase + "', filter='" + filterBuffer + "'"); }

NamingEnumeration results = null;

try

{ results = ctx.search(searchBase, filterBuffer.toString(), ctls); }

catch (NamingException e)

{ log.error("Connected to LDAP, but could not perform " + (env.containsKey(Context.SECURITY_PRINCIPAL) ? "authenticated" : "anonymous") + " search from base '" + searchBase + "'"); return tryOtherCredentialsProviders(name, password); }

StringBuffer dnBuffer = new StringBuffer();

try {
if (log.isDebugEnabled()) {
if ((results != null) && results.hasMore())

{ log.debug("Found users"); }

else

{ log.debug("No users found"); }

}

// Lotus Domino R5 LDAP server drops socket connection when
// switching credentials! First context (ctx) is used to iterate
// through results and only second context (ctx2) is used
// to authorize. Context ctx will survive whether ctx2 is dropped
// by Domino or not.
DirContext ctx2 = new InitialDirContext(ctx.getEnvironment());

while ((results != null) && results.hasMore()) {
SearchResult sr = (SearchResult) results.next();
dnBuffer = new StringBuffer();
dnBuffer.append(sr.getName());
dnBuffer.append(",");
dnBuffer.append(searchBase);

try

{ ctx2.removeFromEnvironment(Context.SECURITY_PRINCIPAL); ctx2.removeFromEnvironment(Context.SECURITY_CREDENTIALS); ctx2.addToEnvironment(Context.SECURITY_PRINCIPAL, dnBuffer.toString()); ctx2.addToEnvironment(Context.SECURITY_CREDENTIALS, password); }

catch (NamingException e)

{ log.error("Connected and searched LDAP, but encountered unexpected error when switching authentication details.", e); continue; }

ctls = new SearchControls();
ctls.setReturningAttributes(new String[0]);
ctls.setSearchScope(SearchControls.OBJECT_SCOPE);

if (log.isDebugEnabled())

{ log.debug("Searching below '" + dnBuffer + "' for '" + filterBuffer + "'"); }

try {
try

{ ctx2.search(dnBuffer.toString(), filterBuffer.toString(), ctls); }

catch (CommunicationException ex)

{ log.info("Second phase connection failed. Trying to reconnect to bypass sort of Lotus Domino R5 problems."); // Second context (ctx2) lost; we recreate this context and try once more. ctx2 = new InitialDirContext(ctx2.getEnvironment()); ctx2.search(dnBuffer.toString(), filterBuffer.toString(), ctls); }

} catch (AuthenticationException ae) {
if (log.isDebugEnabled())

{ log.debug("User with dn '" + dnBuffer + "' found, but authentication failed."); }

continue;
} catch (NamingException e)

{ log.error("Initial connect and search successful, but second phase connection to LDAP as '" + dnBuffer + "' failed.", e); continue; }

if (log.isDebugEnabled())

{ log.debug("User '" + name + "' successfully authenticated; caching for " + timeout + " ms"); }

cache.put(name, new TimeAndPassword(System.currentTimeMillis() +
timeout, password));

return true;
}
} catch (NamingException e)

{ log.error("Connected but encountered error checking if LDAP had more results.", e); }

return tryOtherCredentialsProviders(name, password);
}

public boolean changePassword(String name, String password) {
// LDAP cannot change passwords, so look for the first
CredentialsProvider that can and wrap the call
Collection credentialsProviders =
UserManager.getInstance().getCredentialsProviders();

for (Iterator iterator = credentialsProviders.iterator();
iterator.hasNext() {
CredentialsProvider provider = (CredentialsProvider) iterator.next();
boolean isLDAP = provider instanceof LDAPCredentialsProvider;

if (!isLDAP) {
if (provider.handles(name))

{ return provider.changePassword(name, password); }

}
}

return false;
}

public boolean create(String name)

{ // LDAP doesn't support this feature, so instead call the first provider that does return false; }

public void flushCaches()

{ // flush the TimeAndPassword cache cache = new HashMap(); }

public boolean handles(String name) {
// check cache
TimeAndPassword tp = (TimeAndPassword) cache.get(name);

if ((tp != null) && (tp.time > System.currentTimeMillis())) {
if (log.isDebugEnabled())

{ log.debug("Cached lookup: Credentials for '" + name + "' will be handled by LDAP provider"); }

return true;
}

// check other credentials providers, don't handle this name if at least
one other one doesn't
Collection credentialsProviders =
UserManager.getInstance().getCredentialsProviders();
boolean handles = false;

for (Iterator iterator = credentialsProviders.iterator();
iterator.hasNext() {
CredentialsProvider provider = (CredentialsProvider) iterator.next();
boolean isLDAP = provider instanceof LDAPCredentialsProvider;

if (!isLDAP) {
if (provider.handles(name)) {
if (log.isDebugEnabled())

{ log.debug("'" + name + "' will be handled by LDAP"); }

handles = true;

break;
}
}
}

if (log.isDebugEnabled()) {
if (handles == false)

{ log.debug("Credentials for '" + name + "' NOT handled by LDAP, because '" + name + "' not handled by any other credentials provider. Check you have at least one other" + " credentials provider, and that they contain this user."); }

}

return handles;
}

public boolean init(Properties properties) {
if (log.isDebugEnabled())

{ log.debug("LDAPCredentialsProvider $Revision: 1.3 $ initializing"); }

env = new Hashtable(properties);
env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "simple");
searchBase = properties.getProperty("searchBase");
uidSearchName = properties.getProperty("uidSearchName");

try

{ timeout = Long.parseLong(properties.getProperty("cacheTimeout")); }

catch (NumberFormatException e)

{ timeout = 1000 * 60 * 30; // 30 minutes default time }

return true;
}

public List list()

{ // LDAP cannot list users return Collections.EMPTY_LIST; }

public boolean load(String name, Entity.Accessor accessor) {
// LDAP doesn't support this feature, so instead call the first provider
that does
Collection credentialsProviders =
UserManager.getInstance().getCredentialsProviders();

for (Iterator iterator = credentialsProviders.iterator();
iterator.hasNext() {
CredentialsProvider provider = (CredentialsProvider) iterator.next();
boolean isLDAP = provider instanceof LDAPCredentialsProvider;

if (!isLDAP) {
if (provider.handles(name))

{ return provider.load(name, accessor); }

}
}

return true;
}

public boolean remove(String name) {
// LDAP cannot support removing users
Collection credentialsProviders =
UserManager.getInstance().getCredentialsProviders();

for (Iterator iterator = credentialsProviders.iterator();
iterator.hasNext() {
CredentialsProvider provider = (CredentialsProvider) iterator.next();
boolean isLDAP = provider instanceof LDAPCredentialsProvider;

if (!isLDAP) {
if (provider.handles(name))

{ return provider.remove(name); }

}
}

return false;
}

public boolean store(String name, Entity.Accessor accessor) {
// LDAP doesn't support this feature, so instead call the first provider
that does
Collection credentialsProviders =
UserManager.getInstance().getCredentialsProviders();

for (Iterator iterator = credentialsProviders.iterator();
iterator.hasNext() {
CredentialsProvider provider = (CredentialsProvider) iterator.next();
boolean isLDAP = provider instanceof LDAPCredentialsProvider;

if (!isLDAP) {
if (provider.handles(name))

{ return provider.store(name, accessor); }

}
}

return true;
}

/** Loop through non-LDAP credentials providers and tries to authenticate
against them. */
private boolean tryOtherCredentialsProviders(String name, String password) {
if (log.isDebugEnabled())

{ log.debug("Couldn't authenticate against LDAP server, trying other CredentialsProviders"); }

Collection credentialsProviders =
UserManager.getInstance().getCredentialsProviders();

for (Iterator iterator = credentialsProviders.iterator();
iterator.hasNext() {
CredentialsProvider provider = (CredentialsProvider) iterator.next();
boolean isLDAP = provider instanceof LDAPCredentialsProvider;

if (!isLDAP) {
if (provider.handles(name)) {
if (log.isDebugEnabled())

{ log.debug("Provider '" + provider.getClass().getName() + "' handles user; checking authentication..."); }

boolean result = provider.authenticate(name, password);

if (result) {
if (log.isDebugEnabled())

{ log.debug("User authenticated by '" + provider.getClass().getName() + "'"); }

cache.put(name, new
TimeAndPassword(System.currentTimeMillis() + timeout, password));

return true;
} else {
if (log.isDebugEnabled())

{ log.debug("Provider '" + provider.getClass().getName() + "' failed to authenticate user."); }

return false;
}
}
}
}

if (log.isDebugEnabled())

{ log.debug("No non-LDAP authenticators could authenticate this user"); }

return false;
}

//~ Inner Classes //////////////////////////////////////////////////////////

private class TimeAndPassword {
public String password;
public long time;

public TimeAndPassword(long time, String password)

{ this.time = time; this.password = password; }

}
}


Generated at Sun May 03 22:20:39 UTC 2015 using JIRA 6.2.3#6260-sha1:63ef1d6dac3f4f4d7db4c1effd405ba38ccdc558.