Documented the new authentication methods (LDAP-193).
This commit is contained in:
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user