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">
|
<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>
|
||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user