PDA

View Full Version : Authentication with LDAP or POP3?


kajism
Aug 20th, 2004, 01:05 AM
Hi,

I was wondering if it is possible to do the verification of provided username and password using LDAP (Active Directory) or POP3 server to avoid the need for separate username/password pairs for different corporate systems.

At the moment I'm using DaoAuthenticationProvider and I store usernames and passwords in DB separately for my intranet app.
To implement the POP3 authentication I need to verify the password but I never get it, because there is only username passed into the loadUserByUsername(username) method.

Is there any possible solution to this problem in Acegi?

Thanks,
Karel

Ben Alex
Aug 20th, 2004, 02:03 AM
Could you use the new JaasAuthenticationProvider along with one of the existing JAAS-based LDAP LoginModules that are out there are present?

You could write a new AuthenticationProvider that simply tries to login to the POP3 account using the presented username and password. But I would caution against that as it's fairly ugly (getting a mail server involved with authentication) and for similar effort you could bind against the LDAP server.

Or, you could write an LDAP-based AuthenticationDao. For that to work you would need to connect to the LDAP server with an account capable of reading the password properties of LDAP users, because as you point out you never get the authentication request's password passed to the AuthenticationDao.

The best option would be to write an LDAP AuthenticationProvider. This would bind to the LDAP server with the presented username and password. Thus even if the LDAP server stores it in an encoded form or simply doesn't return the password, you'd still in effect validate the credentials by the act of binding to the server. Don't forget about caching if you go this part. A cache would be a good idea (like UserCache in the DaoAuthenticationProvider land) to avoid needless round-trips to the LDAP server.

Of course, code contributions are always welcome, as LDAP comes up reasonably frequently (and unfortunately I don't have time to setup an LDAP server simply to write an LdapAuthenticationProvider).

kajism
Aug 20th, 2004, 02:34 AM
Ben, Thanks for explaining the possibilities. It seems I will have to play with that in near future, so when I create something working I will be pleased to contribute it.

kajism
Aug 24th, 2004, 03:28 AM
Ben,

I have created a new AuthenticationProvider based on DaoAuthenticationProvider called PasswordDaoAuthenticationProvider. It works in a similar way as DaoAuthenticationProvider but delegates the responsibility of password validation to the PasswordAuthenticationDao:


public interface PasswordAuthenticationDao {
public UserDetails loadUserByUsernameAndPassword(String username, String password)
throws DataAccessException, BadCredentialsException;
}


So this provider could be used in environments where it is not possible to return the (encoded) password. And for example LdapPasswordAuthenticationDaoImpl can be written. Inside of this DAO implementation can be combined access into any DB or LDAP (DB for loading user object and LDAP for username/password validation).

I have also created appropriate PasswordUserCache, because username+password must be the cache key in this case.

All the created classes and interfaces are distinguished by the "Password" prefix and the rest of the names is the same as in the current DaoAuthentication case.

Let me know if you would like to add it into acegisecurity or not.
If yes we can think about better names for the classes and I could write some unit tests for it.

I'm sending you the sources by email.

Karel

Ben Alex
Aug 25th, 2004, 02:18 AM
Hi Karel

Quite a good approach. I did think about whether this would be better in DaoAuthenticationProvider itself, but I think you're approach is the right one given it would make DaoAuthenticationProvider so much more complex with unclear responsibilities compared with its associated DAO.

If you'd like to provide some unit tests and hopefully an LDAP implementation for your new package, I'd be very pleased to add them to CVS.

kajism
Aug 25th, 2004, 06:23 AM
Ok great, I will review the javadoc comments, create tests and send it to you. Then I will start to play games with the LdapPasswordAuthenticationDaoImpl .

Ben Alex
Aug 29th, 2004, 10:40 PM
As per my off-list reply, I've checked the initial code into CVS. Just waiting on the LDAP-specific PasswordAuthenticationDao now.

dmiller
Sep 2nd, 2004, 10:20 AM
kajism, have you gotten anywhere with the LdapPasswordAuthenticationDaoImpl implementation yet? I am working on a project that would benefit from having the functionality that has been discussed in this thread.

Thanks.

Ben Alex
Sep 2nd, 2004, 04:52 PM
Karel sent me a basic LDAP implementation and unit test, but I haven't got an LDAP server setup to test it. If some people would volunteer to integration test it, I'd be happy to add to CVS.

Stephen Baishya
Sep 10th, 2004, 01:36 PM
Hi,

I have written an LDAP Authentication Provider. If anyone is interested I can see if my employer would let me contribute it.

Regards,

Stephen Baishya

Ben Alex
Sep 10th, 2004, 09:44 PM
Hi Stephen: does it use PasswordAuthenticationDao? I already have Karel's original plus some enhancements provided by Daniel Miller. I have just committed them to a new "sandbox" area of Acegi Security CVS, so please feel free to try them out and submit any improvements.

Stephen Baishya
Sep 11th, 2004, 10:02 AM
No, it's a non-DAO provider. DAO didn't seem appropriate for LDAP as you would most commonly use a 'bind' to authenticate rather than retrieving the password.

It supports the following:
Authenticate via bind.
Authorise via group membership (i.e. a role is represented by a group, which itself can be a member of another group up to a configurable level of nesting).
Authorise via a role attribute on the prinicipal object.

Ben Alex
Sep 11th, 2004, 05:23 PM
LdapAuthenticationPasswordDao provides authentication via binding. In fact that is why a new PasswordAuthenticationDao was created. Your GrantedAuthority resolution sounds interesting. It would be good if we could combine the two efforts. It was felt designing the PasswordAuthenticationDao approach was optimal as it allows the use of a pluggable DAO whilst still enjoying provider-managed caching etc.

Mark Pollack
Sep 15th, 2004, 03:37 PM
Hi,

I'm just getting familiar with Acegi today and am trying to map a role/group/user authorization approach onto Acegi. Obviously, these concepts aren't exposed explicitly in Acegi and it is up to an user impl to provide this functionality with a custom AuthenticationDao. (Right?) It seems Stephen has gone down the path of managing the mapping of to Roles and Groups to GrantedAuthorities. I'd be interested in hearing about that in more detail. Seems like an impl that supports role/group/user based authorization would be a nice out of the box feature to have. Just my 2 cents, I'm just getting my sea legs with Acegi - great work btw!

Cheers,
Mark

Ben Alex
Sep 15th, 2004, 07:04 PM
Hi Mark

Yes, you're right in that Acegi Security does not provide group support out-of-the-box. Nor does it really provide role support out-of-the-box.

We have gone with a more flexible GrantedAuthority[] approach. This allows developers to reflect whatever assignable permissions they like. If all they need is a simple RBAC or group model, they can just use a very simple GrantedAuthority implementation called GrantedAuthorityImpl. If on the other hand they need to consider in access control decisions whether a permission was obtained directly or via a group, they can easily create an AuthorityFromGroup implementation which notes the group. The onus is on the DAO which returns the GrantedAuthority[] to fill it with appropriate instances to reflect the security needs of the application. I think this is a reasonable approach, given the types of permissions (GrantedAuthority implementations) developers may need to reflect could vary widely between applications.

There is an LDAP-based DAO in the Acegi Security CVS sandbox. You might be interested in taking a look at it and building group support on it (perhaps it already has it - I haven't had a good look). The same goes for the JdbcDaoImpl. I have no problems if people wish to add group support to the DAOs. We just need to keep any group management responsibility in the actual DAO implementations, and not at a higher layer.

dmiller
Sep 15th, 2004, 11:04 PM
I can verify that the LdapPassowrdAuthenticationDAO in the sandbox does have very simple group -> GrantedAuthority (role like) support. It is very simple because it only reads groups that the authenticated user is a direct "memberOf". A more thorough solution would be to recurse into each group and find all associated groups. This should be fairly straight-forward to implement.

On a related topic, the "memberOf" attribute in Active Directory is a multi-valued attribute. Each value can contain characters like comma, equals, and space (e.g. value="CN=My Group,DC=mydomain,DC=com"). In this case, the attribute value cannot be used directly in the GrantedAuthority (at least with the out-of-the-box role-name-matching support) because it contains those special characters. My solution to this is to transform the special characters into some other character such as underscore making the resulting group name something like this: CN_My_Group_DC_mydomain_DC_com which is a valid name for a GrantedAuthority. It may be a good idea to add a transformation function to the current LdapPassowrdAuthenticationDAO.

Ben Alex
Sep 16th, 2004, 03:16 AM
LdapPasswordAuthenticationDao doesn't really have a maintainer, so any volunteers, patches, contributions etc are welcome.

Stephen Baishya
Sep 20th, 2004, 10:14 AM
Looking at the DAO in the sandbox, it's pretty similar to what I have, with the exception of the recursion into nested groups. As Daniel says, this is not hard to implement.

Some comments/suggestions:

It would be nice to be able to modify the return value of getUserPrincipal and getUsernameAttributes completely through configuration, rather than requiring subclassing. I'm referring specifically to the hard-coded "cn=" and "distinguishedName". As an example, we use "uid=" and "uniquemember", and I wouldn't want to have to subclass to handle this difference.

Don't assume that groups are under the same subtree as users - they frequently are not.

Always return a dummy GrantedAuthority for all authenticated users. The current DAO will throw a BadCredentialsException if the user has no granted authorities. One might want to allow all authenticated users access to something without the overhead of setting up role data in the LDAP directory. By the way, I think this use of BadCredentialsException is wrong - the credentials are fine.

Other than that nice job!

Regards,

Stephen Baishya

asmith
Sep 28th, 2004, 04:59 PM
Instead of writing my own I just did something like this:

JNDIRealmAuthenticationDao extends JNDIRealm
implements PasswordAuthenticationDao


That lets me use the same properties that I use for my JNDI tomcat realm stuff for acegi. see http://jakarta.apache.org/tomcat/tomcat-5.0-doc/realm-howto.html#JNDIRealm Putting it in the acegi distro would add a dependency on

org.apache.catalina.realm.JNDIRealm
org.apache.catalina.realm.GenericPrincipal


but the implementation is trivial and I prefer the configuration syntax/flexibility of the JNDIRealm. Maybe those could be added to catalina-extracted.jar?

This uses code from the ldap dao to handle granted authorities.


public class JNDIRealmAuthenticationDao extends JNDIRealm
implements PasswordAuthenticationDao {
public static final String BAD_CREDENTIALS_EXCEPTION_MESSAGE = "Invalid username, password or context";
private static final transient Log log = LogFactory.getLog(JNDIRealmAuthenticationDao.class );

public UserDetails loadUserByUsernameAndPassword(String username,
String password) throws DataAccessException, BadCredentialsException {
GenericPrincipal principal = null;

Principal tmpPrincipal = authenticate(username, password);

if ((tmpPrincipal == null) || !(tmpPrincipal instanceof GenericPrincipal)) {
if (log.isDebugEnabled()) {
log.debug("principal could not be found");
}

throw new BadCredentialsException(BAD_CREDENTIALS_EXCEPTION_ MESSAGE);
}

principal = (GenericPrincipal) tmpPrincipal;

String[] roles = principal.getRoles();

return new User(username, password, true, getGrantedAuthorities(roles));
}


protected GrantedAuthority[] getGrantedAuthorities(String[] ldapRoles) {
GrantedAuthority[] grantedAuthorities = new GrantedAuthority[ldapRoles.length];

for (int i = 0; i < ldapRoles.length; i++) {
grantedAuthorities[i] = getGrantedAuthority(ldapRoles[i]);
}

return grantedAuthorities;
}

protected GrantedAuthority getGrantedAuthority(String ldapRole) {
GrantedAuthority ga = new GrantedAuthorityImpl("ROLE_" +
ldapRole.toUpperCase());

if (log.isDebugEnabled()) {
log.debug("GrantedAuthority: " + ga);
}

return ga;
}
}


and the corresponding config from applicatonConfig.xml

<bean id="authenticationDao"
class="net.sf.acegisecurity.providers.dao.ldap.SimpleLdap PasswordAuthenticationDao">
<property name="connectionURL">
<value>ldap://localhost:389</value>
</property>
<property name="userPattern">
<value>uid={0},ou=people,dc=mydomain,dc=com</value>
</property>
<property name="roleBase">
<value>ou=groups,dc=mydomain,dc=com</value>
</property>
<property name="roleName">
<value>cn</value>
</property>
<property name="roleSearch">
<value>(uniqueMember={0})</value>
</property>
</bean>


-a.

Ben Alex
Sep 30th, 2004, 06:25 AM
Hi Andrew

I'd be happy to add this to the sandbox.

Is there an easy way to unit test this? My major problem with the current LDAP provider in the sandbox is unit testing. It's difficult to do so without an LDAP server. How do people unit test LDAP typically? A mock, a stub, a lightweight in-memory LDAP server, or don't bother?

If someone can offer some suggestions on unit testing LDAP I would be only too happy to get this put into core and write some docs. This thread has proven extremely popular so it would appear there's interest in LDAP for Acegi Security.

crazeinc
Dec 25th, 2004, 12:53 AM
To remove unnecesary clutter, I deleted a few of my last posts in this thread if you're wondering where they went. I want to use Acegi with my Ldap server, but the LdapDao in the CVS sandbox and the JNDIRealm DAO above won't work with my application. It's for my school and I have zero access to the ldap server to be able to add roles to it and the users that belong to those roles. It's handled by an outside service, updated weekly, and I wouldn't even know where to begin on how to convince them to add it when I'm just a student worker.

So instead I added a property to the applicationContext xml where I specify the cn's I want to have administration access to my app and everyone else is just supposed to have a regular user status. Given my limited java and acegi experience this may be a total hack, but I've posted the class with the hope I can get some feedback and suggestions on it. It's not done yet, but I am able to login and the authorization seems to be working properly on the website. I'm not actually doing anything useful with it yet, but I'm happy to have made some progress.

There are two things I'd like to figure out right away. One is being able to grab the entry data from the user that logged in and store it in the session so I can access things like their email, first/last name, etc without hitting ldap constantly. The second thing is to be able to put the filter string in the applicationContext.xml file instead of hard-coding it in the class. Any suggestions would be great. For the rest of the weekend I'm gonna see if I can figure out how to do a ldap version of the user cache.

LdapDaoImpl.java

/*
* email me if you have a better way of doing this
* pjhyett@gmail.com
*/
package org.appfuse.dao;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;

import javax.naming.AuthenticationException;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;

import net.sf.acegisecurity.BadCredentialsException;
import net.sf.acegisecurity.GrantedAuthority;
import net.sf.acegisecurity.GrantedAuthorityImpl;
import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.providers.dao.PasswordAuthent icationDao;
import net.sf.acegisecurity.providers.dao.User;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureE xception;

public class LdapDaoImpl implements PasswordAuthenticationDao {
private final Log logger = LogFactory.getLog(LdapDaoImpl.class);
public static final String BAD_CREDENTIALS_EXCEPTION_MESSAGE = "Invalid username, password or context";

//~ Instance fields ================================================== ======

private String filter;
private String host;
private String rootContext;
private String[] supervisors;
private String[] attributes;
private int port = 389;

//~ Methods ================================================== ==============

/**
* Set cn's of users meant to be supervisors
*
* @param supervisors The supervisors to set.
*/
public void setSupervisors(String[] supervisors) {
this.supervisors = supervisors;
}

/**
* Set attributes for ldap search
*
* @param attributes The attributes to set.
*/
public void setAttributes(String[] attributes) {
this.attributes = attributes;
}

/**
* Set filter for ldap search
*
* @param filter The filter to set.
*/
public void setFilter(String filter) {
this.filter = filter;
}

/**
* Set hostname or IP address of the host running LDAP server.
*
* @param hostname DOCUMENT ME!
*/
public void setHost(String hostname) {
this.host = hostname;
}

/**
* Set the port on which is running the LDAP server.
*
* @param port DOCUMENT ME!
*/
public void setPort(int port) {
this.port = port;
}

/**
* Set the root context to which you attempt to log in.
*
* @param rootContext DOCUMENT ME!
*/
public void setRootContext(String rootContext) {
this.rootContext = rootContext;
}

public UserDetails loadUserByUsernameAndPassword(String username, String password)
throws DataAccessException, BadCredentialsException {
if ((username == null) || (username.length() == 0)) {
throw new BadCredentialsException("Empty username");
}

if ((password == null) || (password.length() == 0)) {
throw new BadCredentialsException("Empty password");
}

Hashtable env = new Hashtable(11);
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");

StringBuffer providerUrl = new StringBuffer();
providerUrl.append("ldap://");
providerUrl.append(this.host);
providerUrl.append(":");
providerUrl.append(this.port);
providerUrl.append("/");
providerUrl.append(this.rootContext);

String myDN = "cn=" + username + ", " + this.rootContext;

env.put(Context.PROVIDER_URL, providerUrl.toString());
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, myDN);
env.put(Context.SECURITY_CREDENTIALS, password);

try {
DirContext ctx = new InitialDirContext(env);

this.filter = "(&(objectclass=person)(cn="+username+"))";
SearchControls ctls = new SearchControls();
ctls.setReturningAttributes(this.attributes);
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration answer = ctx.search("", this.filter, ctls);
ctx.close();

if(!answer.hasMore()){
//username not found
throw new BadCredentialsException(BAD_CREDENTIALS_EXCEPTION_ MESSAGE);
}else{
//username found
List roles = new ArrayList();
roles.add(new GrantedAuthorityImpl("USER"));

for(int i=0;i<this.supervisors.length;i++){
if(username.equals(this.supervisors[i])){
roles.add(new GrantedAuthorityImpl("SUPERVISOR"));
break;
}
}

/*
* in here would be where I grab the entry details
* and store them in a subclass of User
*/

return new User(username, password, true, getGrantedAuthorities(roles));
}
} catch (AuthenticationException ex) {
throw new BadCredentialsException(BAD_CREDENTIALS_EXCEPTION_ MESSAGE, ex);
} catch (CommunicationException ex) {
throw new DataAccessResourceFailureException(ex.getRootCause ().getMessage(), ex);
} catch (NamingException ex) {
throw new DataAccessResourceFailureException(ex.getMessage() , ex);
}
}

protected GrantedAuthority[] getGrantedAuthorities(List roles) {
GrantedAuthority[] grantedAuthorities = new GrantedAuthority[roles.size()];
for (int i = 0; i < roles.size(); i++) {
grantedAuthorities[i] = getGrantedAuthority(roles.get(i));
}
return grantedAuthorities;
}

protected GrantedAuthority getGrantedAuthority(Object role) {
GrantedAuthority ga = new GrantedAuthorityImpl("ROLE_" + String.valueOf(role).toUpperCase());
return ga;
}
}


applicationContext.xml

<bean id="authenticationDao" class="org.appfuse.dao.LdapDaoImpl">
<property name="port"><value>389</value></property>
<property name="host"><value>ldap.noctrl.edu</value></property>
<property name="rootContext"><value>ou=Napvil,o=NCC</value></property>
<property name="supervisors"><value>pjhyett,mpohl</value></property>
<property name="attributes"><value>mail,sn,GivenName,employeeID</value></property>
</bean>

sarvananda
Jul 23rd, 2007, 05:50 AM
Hi,

Did your employer approve of this ? I am currently in the phase of providing authentication from MySql and Ldap. For Employees of type 'E' the authentication will happen from LDAP. Any pointers in this direction will be highly appreciated

tia