diff --git a/src/docbkx/user-authentication.xml b/src/docbkx/user-authentication.xml index 40817f5d..60222f27 100644 --- a/src/docbkx/user-authentication.xml +++ b/src/docbkx/user-authentication.xml @@ -1,101 +1,255 @@ - - + User Authentication using Spring LDAP - - While the core functionality of the ContextSource is to provide - DirContext instances for use by LdapTemplate, - it may also be used for authenticating users against an LDAP server. The - getContext(principal, credentials) method of ContextSource - will do exactly that; construct a DirContext instance according to the - ContextSource configuration, authenticating the context using the - supplied principal and credentials. - - - Using <literal>ContextSource</literal> for user authentication - -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); - } -} - - - - - The userDn supplied to the authenticate method needs to be the full - DN of the user to authenticate (regardless of the base setting on the - ContextSource). You will typically need to perform an LDAP search - based on e.g. the user name to get this DN: - - - - Finding a user based on uid attribute. -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); -} - - - - - Some authentication schemes and LDAP servers require some operation to be - performed on the created DirContext 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. - - - - Performing LDAP operation on returned <literal>DirContext</literal> objects. - -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); - } -} - - - - - 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 - Spring Security for your security purposes instead. - It is a full-blown, mature security framework addressing the above aspects as well as several others. - + + + Basic Authentication + + While the core functionality of the ContextSource + is to provide DirContext instances for use by + LdapTemplate, it may also be used for authenticating + users against an LDAP server. The getContext(principal, + credentials) method of ContextSource will do + exactly that; construct a DirContext instance according + to the ContextSource configuration, authenticating the + context using the supplied principal and credentials. A custom + authenticate method could look like this: + + + Using <literal>ContextSource</literal> for user + authentication + + 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); + } +} + + + The userDn supplied to the authenticate method + needs to be the full DN of the user to authenticate (regardless of the + base setting on the ContextSource). + You will typically need to perform an LDAP search based on e.g. the user + name to get this DN: + + + Finding a user based on uid attribute. + + 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); +} + + + 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: + + + + boolean authenticate(Name base, String filter, String password); + + + + boolean authenticate(String base, String filter, String password); + + + + Using one of these methods, authentication becomes as simple as + this: + + + Authenticating a user using Spring LDAP. + + boolean authenticated = ldapTemplate.authenticate("", "(uid=john.doe)", "secret"); + + + + Don't write your own custom authenticate methods. Use the ones + provided in Spring LDAP 1.3.x. + + + + + Performing Operations on the Authenticated Context + + Some authentication schemes and LDAP servers require some operation + to be performed on the created DirContext 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 lookup operation is performed + on the authenticated context: + + + Performing an LDAP operation on returned + <literal>DirContext</literal> objects. + + 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); + } +} + + + 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 lookup. Spring LDAP 1.3.0 introduced the + callback interface + AuthenticatedLdapEntryContextCallback and a few + corresponding authenticate methods: + + + + boolean authenticate(Name base, String filter, String password, + AuthenticatedLdapEntryContextCallback callback); + + + + boolean authenticate(String base, String filter, String password, + AuthenticatedLdapEntryContextCallback callback); + + + + This opens up for any operation to be performed on the authenticated + context: + + + Performing an LDAP operation on the authenticated context using + Spring LDAP. + + 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)); + + + + + Retrieving the Authentication Exception + + 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 + AuthenticationErrorCallback and two more + authenticate methods: + + + + boolean authenticate(Name base, String filter, String password, + AuthenticatedLdapEntryContextCallback callback, + AuthenticationErrorCallback errorCallback); + + + + boolean authenticate(String base, String filter, String password, + AuthenticatedLdapEntryContextCallback callback, + AuthenticationErrorCallback errorCallback); + + + + A convenience implementation of the error callback interface is also + provided: + + + Convenience implementation of + <literal>AuthenticationErrorCallback</literal>. + + public static final class CollectingErrorCallback implements AuthenticationErrorCallback { + private Exception error; + + public void execute(Exception e) { + this.error = e; + } + + public Exception getError() { + return error; + } +} + This means that the code needed for authenticating a user and + retrieving the authentication exception in case of an error boils down to + this: + + + Authenticating a user and retrieving the authentication + exception. + + 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 +} + + + + Don't write your own custom authenticate methods that operate on + the authenticated context. Use the ones provided in Spring LDAP + 1.3.1. + + + + + Use Spring Security + + 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 Spring + Security for your security purposes instead. It is a full-blown, + mature security framework addressing the above aspects as well as several + others. + \ No newline at end of file diff --git a/test/integration-tests/src/test/java/org/springframework/ldap/LdapTemplateAuthenticationITest.java b/test/integration-tests/src/test/java/org/springframework/ldap/LdapTemplateAuthenticationITest.java index 8b7a78c8..11765ccb 100644 --- a/test/integration-tests/src/test/java/org/springframework/ldap/LdapTemplateAuthenticationITest.java +++ b/test/integration-tests/src/test/java/org/springframework/ldap/LdapTemplateAuthenticationITest.java @@ -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();