You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by Mario Ivankovits <ma...@ops.co.at> on 2003/05/14 20:02:43 UTC

CLIENT-CERT and JNDI

Hello !

I have tried to use CLIENT-CERT to authenticate the user for our
application. JNDIRealm do not support such authentication, so i have tried
to implement it. For our infrastructure my solution works well, but i think
(know) it is strongly bound to it.

The way it works is to get a certificate for an user, and import this
certificate to the ActiveDirectory Server. During authentication a user with
the matching certificate is searched, and the "cn" for this user is used
furthermore (getRoles() ...)

First, I have created a new class JNDIRealmCertAD (JNDIRealm Certificate
ActiveDirectory) and introduced a new property "certSearch". (I also have
copied the *Pattern getter/setter for use with certificate, but havent
tested it yet)
Much of the code from JNDIRealm has to be copied, due to the private User
class, however, this class is a prototype.

The advantage (i think) of my solution is, that it does not use the
Cert.getSubjectDN() for the username, instead it is using the "cn" for the
ldap entry returned when searching the corresponding user for the
certificate.
With my class it is possible to use BASIC and CLIENT-CERT and always do have
the same username for the application.
I think the application should not be bothered with the type of
authentication.

However, currently this solution is bound to our Win2000-Domain.

Questions:
*) Are there some standards how to map an certificate to an user within an
ldap-server
*) If not, could/should we implement some of my code directly in an class
(say) JNDIRealmCert, and one could simply override an abstract "certToUser"
method.

Comments are welcome !!

Ciao,
Mario


   <Realm className="com.ops.webcontrol.tomcat.JNDIRealmCertAD"
   connectionURL="ldap://AD-SERVER:389"
   userBase="CN=Users,dc=DOMAIN,dc=com"
   certSearch="(altSecurityIdentities={0})"
   userSearch="(cn={0})"
   userRoleName="member"
   roleBase="CN=Users,dc=ops,dc=hq"
   roleName="cn"
   roleSearch="(member={0})"
   connectionName="CN=tomcat,CN=Users,DC=DOMAIN,DC=com"
   connectionPassword="*****"
   roleSubtree="true"
   userSubtree="true"
   debug="99" />


package com.ops.webcontrol.tomcat;

import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.realm.JNDIRealm;
import sun.security.util.DerValue;
import sun.security.util.ObjectIdentifier;
import sun.security.x509.X500Name;

import javax.naming.Name;
import javax.naming.NameNotFoundException;
import javax.naming.NameParser;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.io.IOException;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

public class JNDIRealmCertAD extends JNDIRealm
{
 /**
  * The message format used to search for a user by cert, with "{0}" marking
  * the spot where the cert goes.
  */
 protected String certSearch = null;

 /**
  * The MessageFormat object associated with the current
  * <code>certSearch</code>.
  */
 protected MessageFormat certSearchFormat = null;

 /**
  * The message format used to form the distinguished name of a
  * user, with "{0}" marking the spot where the specified username
  * goes.
  */
 protected String certPattern = null;

 /**
  * The MessageFormat object associated with the current
  * <code>certPattern</code>.
  */
 protected MessageFormat certPatternFormat = null;


 private final static ObjectIdentifier EMAIL;

 /**
  * A private class representing a User
  */
 private static class CertUser
 {
  String username = null;
  String dn = null;
  String password = null;
  ArrayList roles = null;

  CertUser(String username, String dn, String password, ArrayList roles)
  {
   this.username = username;
   this.dn = dn;
   this.password = password;
   this.roles = roles;
  }

 }

 static
 {
  try
  {
   EMAIL = new ObjectIdentifier(new int[]{1, 2, 840, 113549, 1, 9, 1});
  }
  catch (IOException e)
  {
   throw (IllegalArgumentException) new
IllegalArgumentException().initCause(e);
  }
 }

 public Principal authenticate(X509Certificate certs[])
 {
  if ((certs == null) || (certs.length < 1))
   return (null);

  // Check the validity of each certificate in the chain
  if (debug >= 1)
   log("Authenticating client certificate chain");

  if (validate)
  {
   for (int i = 0; i < certs.length; i++)
   {
    if (debug >= 2)
     log(" Checking validity for '" +
      certs[i].getSubjectDN().getName() + "'");
    try
    {
     certs[i].checkValidity();
    }
    catch (Exception e)
    {
     if (debug >= 2)
      log("  Validity exception", e);
     return (null);
    }
   }
  }


  String cert;
  try
  {
   X509Certificate userCert = certs[0];
   X500Name user = (X500Name) userCert.getSubjectDN();

   DerValue val = user.findMostSpecificAttribute(EMAIL);
   if (val == null)
   {
    log(" Attribute EMAIL missing in Cert");
    return null;
   }
   String email = val.getAsString();
   X500Name issuer = (X500Name) userCert.getIssuerDN();

   StringBuffer sb = new StringBuffer(250);
   sb.append("X509:");

   sb.append("<I>");
   sb.append("C=");
   sb.append(issuer.getCountry());
   sb.append(",S=");
   sb.append(issuer.getState());
   sb.append(",L=");
   sb.append(issuer.getLocality());
   sb.append(",O=");
   sb.append(issuer.getOrganization());
   sb.append(",OU=");
   sb.append(issuer.getOrganizationalUnit());
   sb.append(",CN=");
   sb.append(issuer.getCommonName());

   sb.append("<S>");
   sb.append("CN=");
   sb.append(user.getCommonName());
   sb.append(",E=");
   sb.append(email);

   cert = sb.toString();
  }
  catch (IOException e)
  {
   log("build cert", e);
   return null;
  }

  // Retrieve user information
  DirContext context = null;
  try
  {
   context = open();

   CertUser user = getCertUser(context, cert);
   if (user == null)
    return (null);

   // Search for additional roles
   List roles = getRoles(context, user);

   // Create and return a suitable Principal for this user
   return (new GenericPrincipal(this, user.username, null, roles));
  }
  catch (NamingException e)
  {
   log("naming exception", e);
   return null;
  }
  finally
  {
   if (context != null)
    release(context);
  }
 }

 /**
  * Return the message format pattern for selecting users in this Realm.
  */
 public String getCertSearch()
 {
  return (this.certSearch);
 }


 /**
  * Set the message format pattern for selecting users in this Realm.
  *
  * @param certSearch The new user search pattern
  */
 public void setCertSearch(String certSearch)
 {

  this.certSearch = certSearch;
  if (certSearch == null)
   certSearchFormat = null;
  else
   certSearchFormat = new MessageFormat(certSearch);

 }

 /**
  * Return the message format pattern for selecting users in this Realm.
  */
 public String getCertPattern()
 {
  return (this.certPattern);
 }


 /**
  * Set the message format pattern for selecting users in this Realm.
  *
  * @param certPattern The new cert pattern
  */
 public void setCertPattern(String certPattern)
 {
  this.certPattern = certPattern;
  if (certPattern == null)
   certPatternFormat = null;
  else
   certPatternFormat = new MessageFormat(certPattern);
 }

 /**
  * Return a List of roles associated with the given User.  Any
  * roles present in the user's directory entry are supplemented by
  * a directory search. If no roles are associated with this user,
  * a zero-length List is returned.
  *
  * @param context The directory context we are searching
  * @param user The User to be checked
  *
  * @exception NamingException if a directory server error occurs
  */
 protected List getRoles(DirContext context, CertUser user) throws
NamingException
 {
  if (user == null)
   return (null);

  String dn = user.dn;
  String username = user.username;

  if (dn == null || username == null)
   return (null);

  if (debug >= 2)
   log("  getRoles(" + dn + ")");

  // Start with roles retrieved from the user entry
  ArrayList list = user.roles;
  if (list == null)
  {
   list = new ArrayList();
  }

  // Are we configured to do role searches?
  if ((roleFormat == null) || (roleName == null))
   return (list);

  // Set up parameters for an appropriate search
  String filter = roleFormat.format(new String[]{dn, username});
  SearchControls controls = new SearchControls();
  if (roleSubtree)
   controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
  else
   controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
  controls.setReturningAttributes(new String[]{roleName});

  // Perform the configured search and process the results
  if (debug >= 3)
  {
   log("  Searching role base '" + roleBase + "' for attribute '" +
    roleName + "'");
   log("  With filter expression '" + filter + "'");
  }
  NamingEnumeration results =
   context.search(roleBase, filter, controls);
  if (results == null)
   return (list);  // Should never happen, but just in case ...
  while (results.hasMore())
  {
   SearchResult result = (SearchResult) results.next();
   Attributes attrs = result.getAttributes();
   if (attrs == null)
    continue;
   list = addAttributeValues(roleName, attrs, list);
  }

  // Return the augmented list of roles
  if (debug >= 2)
  {
   log("  Returning " + list.size() + " roles");
   for (int i = 0; i < list.size(); i++)
    log("  Found role " + list.get(i));
  }

  return (list);
 }

 /**
  * Return a User object containing information about the user
  * with the specified username, if found in the directory;
  * otherwise return <code>null</code>.
  *
  * If the <code>userPassword</code> configuration attribute is
  * specified, the value of that attribute is retrieved from the
  * user's directory entry. If the <code>userRoleName</code>
  * configuration attribute is specified, all values of that
  * attribute are retrieved from the directory entry.
  *
  * @param context The directory context
  * @param username Username to be looked up
  *
  * @exception NamingException if a directory server error occurs
  */
 protected CertUser getCertUser(DirContext context, String username) throws
NamingException
 {

  CertUser user = null;

  // Get attributes to retrieve from user entry
  ArrayList list = new ArrayList();
  list.add("cn");

  if (userPassword != null)
   list.add(userPassword);
  if (userRoleName != null)
   list.add(userRoleName);
  String[] attrIds = new String[list.size()];
  list.toArray(attrIds);

  // Use pattern or search for user entry
  if (certPatternFormat != null)
  {
   user = getCertUserByPattern(context, username, attrIds);
  }
  else
  {
   user = getCertUserBySearch(context, username, attrIds);
  }

  return user;
 }

 /**
  * Use the <code>CertPattern</code> configuration attribute to
  * locate the directory entry for the user with the specified
  * username and return a User object; otherwise return
  * <code>null</code>.
  *
  * @param context The directory context
  * @param username The username
  * @param attrIds String[]containing names of attributes to
  * retrieve.
  *
  * @exception NamingException if a directory server error occurs
  */
 protected CertUser getCertUserByPattern(DirContext context,
           String username,
           String[] attrIds)
  throws NamingException
 {

  if (debug >= 2)
   log("lookupUser(" + username + ")");

  if (username == null || certPatternFormat == null)
   return (null);

  // Form the dn from the user pattern
  String dn = certPatternFormat.format(new String[]{username});
  if (debug >= 3)
  {
   log("  dn=" + dn);
  }

  // Get required attributes from user entry
  Attributes attrs = null;
  try
  {
   attrs = context.getAttributes(dn, attrIds);
  }
  catch (NameNotFoundException e)
  {
   return (null);
  }
  if (attrs == null)
   return (null);

  // Retrieve value of userName
  String cn = getAttributeValue("cn", attrs);

  // Retrieve value of userPassword
  String password = null;
  if (userPassword != null)
   password = getAttributeValue(userPassword, attrs);

  // Retrieve values of userRoleName attribute
  ArrayList roles = null;
  if (userRoleName != null)
   roles = addAttributeValues(userRoleName, attrs, roles);

  return new CertUser(cn, dn, password, roles);
 }

 /**
  * Return a String representing the value of the specified attribute.
  *
  * @param attrId Attribute name
  * @param attrs Attributes containing the required value
  *
  * @exception NamingException if a directory server error occurs
  */
 private String getAttributeValue(String attrId, Attributes attrs) throws
NamingException
 {

  if (debug >= 3)
   log("  retrieving attribute " + attrId);

  if (attrId == null || attrs == null)
   return null;

  Attribute attr = attrs.get(attrId);
  if (attr == null)
   return (null);
  Object value = attr.get();
  if (value == null)
   return (null);
  String valueString = null;
  if (value instanceof byte[])
   valueString = new String((byte[]) value);
  else
   valueString = value.toString();

  return valueString;
 }

 /**
  * Add values of a specified attribute to a list
  *
  * @param attrId Attribute name
  * @param attrs Attributes containing the new values
  * @param values ArrayList containing values found so far
  *
  * @exception NamingException if a directory server error occurs
  */
 private ArrayList addAttributeValues(String attrId, Attributes attrs,
ArrayList values) throws NamingException
 {

  if (debug >= 3)
   log("  retrieving values for attribute " + attrId);
  if (attrId == null || attrs == null)
   return null;
  if (values == null)
   values = new ArrayList();
  Attribute attr = attrs.get(attrId);
  if (attr == null)
   return (null);
  NamingEnumeration e = attr.getAll();
  while (e.hasMore())
  {
   String value = (String) e.next();
   values.add(value);
  }
  return values;
 }

 /**
  * Search the directory to return a User object containing
  * information about the user with the specified cert, if
  * found in the directory; otherwise return <code>null</code>.
  *
  * @param context The directory context
  * @param cert The cert
  * @param attrIds String[]containing names of attributes to retrieve.
  *
  * @exception NamingException if a directory server error occurs
  */
 protected CertUser getCertUserBySearch(DirContext context,
             String cert,
             String[] attrIds)
  throws NamingException
 {

  if (cert == null || certSearchFormat == null)
   return (null);

  // Form the search filter
  String filter = certSearchFormat.format(new String[]{cert});

  // Set up the search controls
  SearchControls constraints = new SearchControls();

  if (userSubtree)
  {
   constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
  }
  else
  {
   constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
  }

  // Specify the attributes to be retrieved
  if (attrIds == null)
   attrIds = new String[0];
  constraints.setReturningAttributes(attrIds);

  if (debug > 3)
  {
   log("  Searching for " + cert);
   log("  base: " + userBase + "  filter: " + filter);
  }

  NamingEnumeration results =
   context.search(userBase, filter, constraints);


  // Fail if no entries found
  if (results == null || !results.hasMore())
  {
   if (debug > 2)
   {
    log("  cert not found");
   }
   return (null);
  }

  // Get result for the first entry found
  SearchResult result = (SearchResult) results.next();

  // Check no further entries were found
  if (results.hasMore())
  {
   log("cert " + cert + " has multiple entries");
   return (null);
  }

  // Get the entry's distinguished name
  NameParser parser = context.getNameParser("");
  Name contextName = parser.parse(context.getNameInNamespace());
  Name baseName = parser.parse(userBase);
  Name entryName = parser.parse(result.getName());
  Name name = contextName.addAll(baseName);
  name = name.addAll(entryName);
  String dn = name.toString();

  if (debug > 2)
   log("  entry found for " + cert + " with dn " + dn);

  // Get the entry's attributes
  Attributes attrs = result.getAttributes();
  if (attrs == null)
   return null;

  // Retrieve value of userName
  String cn = getAttributeValue("cn", attrs);

  // Retrieve value of userPassword
  String password = null;
  if (userPassword != null)
   password = getAttributeValue(userPassword, attrs);

  // Retrieve values of userRoleName attribute
  ArrayList roles = null;
  if (userRoleName != null)
   roles = addAttributeValues(userRoleName, attrs, roles);

  return new CertUser(cn, dn, password, roles);
 }
}

Re: CLIENT-CERT and JNDI

Posted by Mario Ivankovits <ma...@ops.co.at>.
I forgot to write, i currently use Tomcat 4.1.24