Documented the new authentication methods (LDAP-193).

This commit is contained in:
Ulrik Sandberg
2009-12-15 00:09:31 +00:00
parent 81281476b1
commit 2df7de35c2
2 changed files with 276 additions and 98 deletions

View File

@@ -1,101 +1,255 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<chapter id="user-authentication"> <chapter id="user-authentication">
<title>User Authentication using Spring LDAP</title> <title>User Authentication using Spring LDAP</title>
<para>While the core functionality of the <literal>ContextSource</literal> is to provide <sect1>
<literal>DirContext</literal> instances for use by <literal>LdapTemplate</literal>, <title>Basic Authentication</title>
it may also be used for authenticating users against an LDAP server. The
<literal>getContext(principal, credentials)</literal> method of <literal>ContextSource</literal> <para>While the core functionality of the <literal>ContextSource</literal>
will do exactly that; construct a <literal>DirContext</literal> instance according to the is to provide <literal>DirContext</literal> instances for use by
<literal>ContextSource</literal> configuration, authenticating the context using the <literal>LdapTemplate</literal>, it may also be used for authenticating
supplied principal and credentials. users against an LDAP server. The <literal>getContext(principal,
</para> credentials)</literal> method of <literal>ContextSource</literal> will do
<example> exactly that; construct a <literal>DirContext</literal> instance according
<title>Using <literal>ContextSource</literal> for user authentication</title> to the <literal>ContextSource</literal> configuration, authenticating the
context using the supplied principal and credentials. A custom
<programlisting>public boolean authenticate(String userDn, String credentials) { authenticate method could look like this:</para>
DirContext ctx = null;
try { <example>
ctx = contextSource.getContext(userDn, credentials); <title>Using <literal>ContextSource</literal> for user
authentication</title>
return true;
} catch (Exception e) { <programlisting>public boolean authenticate(String userDn, String credentials) {
// Context creation failed - authentication did not succeed DirContext ctx = null;
logger.error("Login failed", e); try {
return false; ctx = contextSource.getContext(userDn, credentials);
} finally { return true;
// It is imperative that the created DirContext instance is always closed } catch (Exception e) {
LdapUtils.closeContext(ctx); // Context creation failed - authentication did not succeed
} logger.error("Login failed", e);
} return false;
</programlisting> } finally {
</example> // It is imperative that the created DirContext instance is always closed
LdapUtils.closeContext(ctx);
<para><note><para> }
The userDn supplied to the <literal>authenticate</literal> method needs to be the full }</programlisting>
DN of the user to authenticate (regardless of the <literal>base</literal> setting on the </example>
<literal>ContextSource</literal>). You will typically need to perform an LDAP search
based on e.g. the user name to get this DN: <para>The userDn supplied to the <literal>authenticate</literal> method
</para></note></para> needs to be the full DN of the user to authenticate (regardless of the
<literal>base</literal> setting on the <literal>ContextSource</literal>).
<example> You will typically need to perform an LDAP search based on e.g. the user
<title>Finding a user based on uid attribute.</title> name to get this DN:</para>
<programlisting>private String getDnForUser(String uid) {
Filter f = new EqualsFilter("uid", uid); <example>
List result = ldapTemplate.search(DistinguishedName.EMPTY_PATH, f.toString(), new AbstractContextMapper() { <title>Finding a user based on uid attribute.</title>
protected Object doMapFromContext(DirContextOperations ctx) {
return ctx.getNameInNamespace(); <programlisting>private String getDnForUser(String uid) {
} Filter f = new EqualsFilter("uid", uid);
}); List result = ldapTemplate.search(DistinguishedName.EMPTY_PATH, f.toString(), new AbstractContextMapper() {
protected Object doMapFromContext(DirContextOperations ctx) {
if(result.size() != 1) { return ctx.getNameInNamespace();
throw new RuntimeException("User not found or not unique"); }
} });
return (String)result.get(0); if(result.size() != 1) {
} throw new RuntimeException("User not found or not unique");
</programlisting> }
</example>
return (String)result.get(0);
<para><note><para> }</programlisting>
Some authentication schemes and LDAP servers require some operation to be </example>
performed on the created <literal>DirContext</literal> instance for the actual
authentication to occur. You should test and make sure how your server setup and <para>There are some drawbacks to this approach. The user is forced to
authentication schemes behave; failure to do so might result in that users concern herself with the DN of the user, she can only search for the
will be admitted into your system regardless of the DN/credentials supplied. user's uid, and the search always starts at the root of the tree (the
</para></note></para> empty path). A more flexible method would let the user specify the search
base, the search filter, and the credentials. Spring LDAP 1.3.0 introduced
<example> new authenticate methods in LdapTemplate that provide this
<title>Performing LDAP operation on returned <literal>DirContext</literal> objects.</title> functionality:</para>
<programlisting>public boolean authenticate(String userDn, String credentials) { <itemizedlist>
DirContext ctx = null; <listitem>
try { <literallayout><literal>boolean authenticate(Name base, String filter, String password);</literal></literallayout>
ctx = contextSource.getContext(userDn, credentials); </listitem>
// Take care here - if a base was specified on the ContextSource
// that needs to be removed from the user DN for the lookup to succeed. <listitem>
ctx.lookup(userDn); <literallayout>boolean authenticate(String base, String filter, String password);</literallayout>
</listitem>
return true; </itemizedlist>
} catch (Exception e) {
// Context creation failed - authentication did not succeed <para>Using one of these methods, authentication becomes as simple as
logger.error("Login failed", e); this:</para>
return false;
} finally { <para><example>
// It is imperative that the created DirContext instance is always closed <title>Authenticating a user using Spring LDAP.</title>
LdapUtils.closeContext(ctx);
} <programlisting>boolean authenticated = ldapTemplate.authenticate("", "(uid=john.doe)", "secret");</programlisting>
} </example></para>
</programlisting>
</example> <important>
<para>Don't write your own custom authenticate methods. Use the ones
<para><note><para> provided in Spring LDAP 1.3.x.</para>
While the approach above may be sufficient for very simple authentication scenarios, requirements in this </important>
area commonly expand rapidly. There is a multitude of aspects that apply to this area, including </sect1>
authentication, authorization, web integration, user context management, etc. If you
suspect that the requirements might expand you should definitely consider using <sect1>
<ulink url="http://springsecurity.org">Spring Security</ulink> for your security purposes instead. <title>Performing Operations on the Authenticated Context</title>
It is a full-blown, mature security framework addressing the above aspects as well as several others.
</para></note></para> <para>Some authentication schemes and LDAP servers require some operation
to be performed on the created <literal>DirContext</literal> instance for
the actual authentication to occur. You should test and make sure how your
server setup and authentication schemes behave; failure to do so might
result in that users will be admitted into your system regardless of the
DN/credentials supplied. This is a naïve implementation of an authenticate
method where a hard-coded <literal>lookup</literal> operation is performed
on the authenticated context:</para>
<example>
<title>Performing an LDAP operation on returned
<literal>DirContext</literal> objects.</title>
<programlisting>public boolean authenticate(String userDn, String credentials) {
DirContext ctx = null;
try {
ctx = contextSource.getContext(userDn, credentials);
// Take care here - if a base was specified on the ContextSource
// that needs to be removed from the user DN for the lookup to succeed.
<emphasis role="bold"> ctx.lookup(userDn);
</emphasis>
return true;
} catch (Exception e) {
// Context creation failed - authentication did not succeed
logger.error("Login failed", e);
return false;
} finally {
// It is imperative that the created DirContext instance is always closed
LdapUtils.closeContext(ctx);
}
}</programlisting>
</example>
<para>It would be better if the operation could be provided as an
implementation of a callback interface, thus not limiting the operation to
always be a <literal>lookup</literal>. Spring LDAP 1.3.0 introduced the
callback interface
<literal>AuthenticatedLdapEntryContextCallback</literal> and a few
corresponding <literal>authenticate</literal> methods:</para>
<itemizedlist>
<listitem>
<literallayout>boolean authenticate(Name base, String filter, String password,
AuthenticatedLdapEntryContextCallback callback);</literallayout>
</listitem>
<listitem>
<literallayout>boolean authenticate(String base, String filter, String password,
AuthenticatedLdapEntryContextCallback callback);</literallayout>
</listitem>
</itemizedlist>
<para>This opens up for any operation to be performed on the authenticated
context:</para>
<example>
<title>Performing an LDAP operation on the authenticated context using
Spring LDAP.</title>
<programlisting>AuthenticatedLdapEntryContextCallback contextCallback = new AuthenticatedLdapEntryContextCallback() {
public void executeWithContext(DirContext ctx, LdapEntryIdentification ldapEntryIdentification) {
try {
ctx.lookup(ldapEntryIdentification.getRelativeDn());
}
catch (NamingException e) {
throw new RuntimeException("Failed to lookup " + ldapEntryIdentification.getRelativeDn(), e);
}
}
};
ldapTemplate.authenticate("", "(uid=john.doe)", "secret", contextCallback));</programlisting>
</example>
</sect1>
<sect1>
<title>Retrieving the Authentication Exception</title>
<para>So far, the methods have only been able to tell the user whether or
not the authentication succeeded. There has been no way of retrieving the
actual exception. Spring LDAP 1.3.1 introduced the
<literal>AuthenticationErrorCallback</literal> and two more
<literal>authenticate</literal> methods:</para>
<itemizedlist>
<listitem>
<literallayout>boolean authenticate(Name base, String filter, String password,
AuthenticatedLdapEntryContextCallback callback,
AuthenticationErrorCallback errorCallback);</literallayout>
</listitem>
<listitem>
<literallayout>boolean authenticate(String base, String filter, String password,
AuthenticatedLdapEntryContextCallback callback,
AuthenticationErrorCallback errorCallback);</literallayout>
</listitem>
</itemizedlist>
<para>A convenience implementation of the error callback interface is also
provided:</para>
<para><example>
<title>Convenience implementation of
<literal>AuthenticationErrorCallback</literal>.</title>
<programlisting>public static final class CollectingErrorCallback implements AuthenticationErrorCallback {
private Exception error;
public void execute(Exception e) {
this.error = e;
}
public Exception getError() {
return error;
}
}</programlisting>
</example>This means that the code needed for authenticating a user and
retrieving the authentication exception in case of an error boils down to
this:</para>
<para><example>
<title>Authenticating a user and retrieving the authentication
exception.</title>
<programlisting>import org.springframework.ldap.core.LdapTemplate.CollectingErrorCallback;
import org.springframework.ldap.core.LdapTemplate.NullAuthenticatedLdapEntryContextCallback;
...
CollectingErrorCallback errorCallback = new CollectingErrorCallback();
AuthenticatedLdapEntryContextCallback callback = new NullAuthenticatedLdapEntryContextCallback();
boolean result = tested.authenticate("", filter.toString(), "invalidpassword", callback, errorCallback);
if (!result) {
Exception error = errorCallback.getError();
// error is likely of type org.springframework.ldap.AuthenticationException
}</programlisting>
</example></para>
<important>
<para>Don't write your own custom authenticate methods that operate on
the authenticated context. Use the ones provided in Spring LDAP
1.3.1.</para>
</important>
</sect1>
<sect1>
<title>Use Spring Security</title>
<para>While the approach above may be sufficient for simple authentication
scenarios, requirements in this area commonly expand rapidly. There is a
multitude of aspects that apply, including authentication, authorization,
web integration, user context management, etc. If you suspect that the
requirements might expand beyond just simple authentication, you should
definitely consider using <ulink type=""
url="http://static.springsource.org/spring-security/site/">Spring
Security</ulink> for your security purposes instead. It is a full-blown,
mature security framework addressing the above aspects as well as several
others.</para>
</sect1>
</chapter> </chapter>

View File

@@ -16,13 +16,19 @@
package org.springframework.ldap; package org.springframework.ldap;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.core.AuthenticatedLdapEntryContextCallback; import org.springframework.ldap.core.AuthenticatedLdapEntryContextCallback;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.LdapEntryIdentification;
import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.LdapTemplate.CollectingErrorCallback; import org.springframework.ldap.core.LdapTemplate.CollectingErrorCallback;
import org.springframework.ldap.core.LdapTemplate.NullAuthenticatedLdapEntryContextCallback; import org.springframework.ldap.core.LdapTemplate.NullAuthenticatedLdapEntryContextCallback;
@@ -57,6 +63,24 @@ public class LdapTemplateAuthenticationITest extends AbstractLdapTemplateIntegra
assertFalse(tested.authenticate("", filter.toString(), "invalidpassword")); assertFalse(tested.authenticate("", filter.toString(), "invalidpassword"));
} }
@Test
public void testAuthenticateWithLookupOperationPerformedOnAuthenticatedContext() {
AndFilter filter = new AndFilter();
filter.and(new EqualsFilter("objectclass", "person")).and(new EqualsFilter("uid", "some.person3"));
AuthenticatedLdapEntryContextCallback contextCallback = new AuthenticatedLdapEntryContextCallback() {
public void executeWithContext(DirContext ctx, LdapEntryIdentification ldapEntryIdentification) {
try {
DirContextAdapter adapter = (DirContextAdapter) ctx.lookup(ldapEntryIdentification.getRelativeDn());
assertEquals("Some Person3", adapter.getStringAttribute("cn"));
}
catch (NamingException e) {
throw new RuntimeException("Failed to lookup " + ldapEntryIdentification.getRelativeDn(), e);
}
}
};
assertTrue(tested.authenticate("", filter.toString(), "password", contextCallback));
}
@Test @Test
public void testAuthenticateWithInvalidPasswordAndCollectedException() { public void testAuthenticateWithInvalidPasswordAndCollectedException() {
AndFilter filter = new AndFilter(); AndFilter filter = new AndFilter();