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">
<title>User Authentication using Spring LDAP</title>
<para>While the core functionality of the <literal>ContextSource</literal> is to provide
<literal>DirContext</literal> instances for use by <literal>LdapTemplate</literal>,
it may also be used for authenticating users against an LDAP server. The
<literal>getContext(principal, credentials)</literal> method of <literal>ContextSource</literal>
will do exactly that; construct a <literal>DirContext</literal> instance according to the
<literal>ContextSource</literal> configuration, authenticating the context using the
supplied principal and credentials.
</para>
<example>
<title>Using <literal>ContextSource</literal> for user authentication</title>
<programlisting>public boolean authenticate(String userDn, String credentials) {
DirContext ctx = null;
try {
ctx = contextSource.getContext(userDn, credentials);
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><note><para>
The userDn supplied to the <literal>authenticate</literal> method needs to be the full
DN of the user to authenticate (regardless of the <literal>base</literal> setting on the
<literal>ContextSource</literal>). You will typically need to perform an LDAP search
based on e.g. the user name to get this DN:
</para></note></para>
<example>
<title>Finding a user based on uid attribute.</title>
<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) {
return ctx.getNameInNamespace();
}
});
if(result.size() != 1) {
throw new RuntimeException("User not found or not unique");
}
return (String)result.get(0);
}
</programlisting>
</example>
<para><note><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.
</para></note></para>
<example>
<title>Performing 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.
ctx.lookup(userDn);
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><note><para>
While the approach above may be sufficient for very simple authentication scenarios, requirements in this
area commonly expand rapidly. There is a multitude of aspects that apply to this area, including
authentication, authorization, web integration, user context management, etc. If you
suspect that the requirements might expand you should definitely consider using
<ulink url="http://springsecurity.org">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></note></para>
<sect1>
<title>Basic Authentication</title>
<para>While the core functionality of the <literal>ContextSource</literal>
is to provide <literal>DirContext</literal> instances for use by
<literal>LdapTemplate</literal>, it may also be used for authenticating
users against an LDAP server. The <literal>getContext(principal,
credentials)</literal> method of <literal>ContextSource</literal> will do
exactly that; construct a <literal>DirContext</literal> instance according
to the <literal>ContextSource</literal> configuration, authenticating the
context using the supplied principal and credentials. A custom
authenticate method could look like this:</para>
<example>
<title>Using <literal>ContextSource</literal> for user
authentication</title>
<programlisting>public boolean authenticate(String userDn, String credentials) {
DirContext ctx = null;
try {
ctx = contextSource.getContext(userDn, credentials);
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>The userDn supplied to the <literal>authenticate</literal> method
needs to be the full DN of the user to authenticate (regardless of the
<literal>base</literal> setting on the <literal>ContextSource</literal>).
You will typically need to perform an LDAP search based on e.g. the user
name to get this DN:</para>
<example>
<title>Finding a user based on uid attribute.</title>
<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) {
return ctx.getNameInNamespace();
}
});
if(result.size() != 1) {
throw new RuntimeException("User not found or not unique");
}
return (String)result.get(0);
}</programlisting>
</example>
<para>There are some drawbacks to this approach. The user is forced to
concern herself with the DN of the user, she can only search for the
user's uid, and the search always starts at the root of the tree (the
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
new authenticate methods in LdapTemplate that provide this
functionality:</para>
<itemizedlist>
<listitem>
<literallayout><literal>boolean authenticate(Name base, String filter, String password);</literal></literallayout>
</listitem>
<listitem>
<literallayout>boolean authenticate(String base, String filter, String password);</literallayout>
</listitem>
</itemizedlist>
<para>Using one of these methods, authentication becomes as simple as
this:</para>
<para><example>
<title>Authenticating a user using Spring LDAP.</title>
<programlisting>boolean authenticated = ldapTemplate.authenticate("", "(uid=john.doe)", "secret");</programlisting>
</example></para>
<important>
<para>Don't write your own custom authenticate methods. Use the ones
provided in Spring LDAP 1.3.x.</para>
</important>
</sect1>
<sect1>
<title>Performing Operations on the Authenticated Context</title>
<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>

View File

@@ -16,13 +16,19 @@
package org.springframework.ldap;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertNotNull;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.CollectingErrorCallback;
import org.springframework.ldap.core.LdapTemplate.NullAuthenticatedLdapEntryContextCallback;
@@ -57,6 +63,24 @@ public class LdapTemplateAuthenticationITest extends AbstractLdapTemplateIntegra
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
public void testAuthenticateWithInvalidPasswordAndCollectedException() {
AndFilter filter = new AndFilter();