vADC Docs

Authenticating users with Active Directory and Stingray Java Extensions

by on ‎03-21-2013 11:15 AM - edited on ‎06-16-2015 05:01 PM by PaulWallace (4,629 Views)

Important note - this article illustrates an example of authenticating traffic using Java Extensions.  Stingray TrafficScript also includes LDAP/Active Directory primitives, in the form of auth.query(), and these are generally simpler and easier to use than a Java-based solution.

 

Overview

 

A very common requirement for intranet and extranet applications is the need to authenticate users against an Active Directory (or LDAP) database. The Java Extension in this article describes how to do exactly that.

 

This article describes two Java Extensions that manage the HTTP Basic Authentication process and validate the supplied username and password against an Active Directory database. It shows how to use Initialization Parameters to provide configuration to an extension, and how authentication results can be cached to reduce the load on the Active Directory server.

 

A basic Java Extension

 

The first Java Extension verifies that the supplied username and password can bind directly to the LDAP database.  It's appropriate for simple LDAP deployments, but enterprise AD deployments may not give end users permissions to bind directly to the entire database, so the second example may be more appropriate.

 

The Java Extension (version 1)

 

import java.io.IOException;  
import java.io.PrintWriter;  
import java.util.Hashtable;  
  
import javax.naming.Context;  
import javax.naming.directory.DirContext;  
import javax.naming.directory.InitialDirContext;  
import javax.servlet.ServletConfig;  
import javax.servlet.ServletException;  
import javax.servlet.http.HttpServlet;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
  
import com.zeus.ZXTMServlet.ZXTMHttpServletRequest;  
  
public class LdapAuthenticate extends HttpServlet {  
    private static final long serialVersionUID = 1L;  
    private String dirServer;  
    private String realm;  
      
    public void init( ServletConfig config) throws ServletException {  
        super.init( config );  
        dirServer = config.getInitParameter( "DB" );  
        realm = config.getInitParameter( "Realm" );  
        if( dirServer == null ) throw new ServletException( "No DB configured" );  
        if( realm == null ) realm = "Secure site";  
    }  
      
    public void doGet( HttpServletRequest req, HttpServletResponse res )  
        throws ServletException, IOException  
    {  
        try {  
            ZXTMHttpServletRequest zreq = (ZXTMHttpServletRequest)req;  
              
            String[] userPass = zreq.getRemoteUserAndPassword();  
            if( userPass == null ) throw new Exception( "No Authentication details" );  
  
            Hashtable<String, String> env = new Hashtable<String, String>();            env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");  
            env.put( Context.PROVIDER_URL, "LDAP://" + dirServer );  
            env.put( Context.SECURITY_AUTHENTICATION, "DIGEST-MD5" );  
            env.put( Context.SECURITY_PRINCIPAL, userPass[0] );  
            env.put( Context.SECURITY_CREDENTIALS, userPass[1] );  
  
            DirContext ctx = new InitialDirContext( env );  
            ctx.close();  
              
            // No exceptions thrown... must have been successful ;-)  
            return;  
        } catch( Exception e ) {  
            res.setHeader( "WWW-Authenticate", "Basic realm=\"" + realm + "\"" );  
            res.setHeader( "Content-Type", "text/html" );  
            res.setStatus( 401 );  
              
            String message =   
                "<html>" +  
                "<head><title>Unauthorized</title></head>" +  
                "<body>" +  
                "<h2>Unauthorized - please log in</h2>" +  
                "<p>Please log in with your system username and password</p>" +  
                "<p>Error: " + e.toString() + "</p>" +  
                "</body>" +  
                "</html>";  
              
            PrintWriter out = res.getWriter();  
            out.println( message );  
        }  
    }  
  
    public void doPost( HttpServletRequest req, HttpServletResponse res )  
        throws ServletException, IOException  
    {  
        doGet( req, res );  
    }  
} 

 

 

Configuring the Java Extension

 

Upload the LdapAuthenticate Java Extension into the Java Catalog page. Click on the LdapAuthenticate link to edit the properties of the extension, and add two Initialization Parameters:

ldapparams1.png

 

DB specifies the name of the LDAP or Active Directory database, and Realm specifies the authentication realm.

 

These parameters are read when the extension is initialized, which occurs the first time the extension is used by Stingray:

 

public void init( ServletConfig config) throws ServletException {  
        super.init( config );  
        dirServer = config.getInitParameter( "DB" );  
        realm = config.getInitParameter( "Realm" );  
        if( dirServer == null ) throw new ServletException( "No DB configured" );  
        if( realm == null ) realm = "Secure site";  
    }  

 

If you change the value of one of these parameters, use the 'Force Reload' option in the Stingray Admin Server to unload and reload this extension.

 

Either use the auto-generated rule, or create a new TrafficScript rule to call the extension on every request to an HTTP virtual server:

 

java.run( "LdapAuthenticate" );

 

Testing the Java Extension

 

When you try to access the web site through Stingray, you will be prompted for a username and password; the LdapAuthenticate extension checks that the username and password can bind to the configured Active Directory database, and refuses access if not:

ldapauth.png

If you are unable to log in, cancel the prompt dialog box to see the error reported by the Java extension. In the following case, there was a networking problem; the extension could not contact the database server provided in the 'DB' parameter:

Screen Shot 2013-03-21 at 18.00.59.png

Caching the Authentication Results

 

Caching the Authentication response from the Java extension will improve the performance of the web site and reduce the load on the database server.

 

You can modify the TrafficScript rule that calls the extension so that it records successful logins, caching them for a period of time. The following rule uses the data.set() TrafficScript function to record successful logins, caching this information for 10 minutes before attempting to reauthenticate the user against the database server.

 

$auth = http.getHeader( "Authorization" );  
  
if( data.get( $auth ) < sys.time() ) {  
   data.remove( $auth );  
   java.run( "LdapAuthenticate" );  
  
   # if we got here, we were authenticated.  
   # Cache this information for 600 seconds  
   data.set( $auth, sys.time()+600 );  
}  

 

A more sophisticated Java Extension implementation

 

In enterprise deployments, users often cannot bind to the LDAP or Active Directory database directly.  This example runs a custom search against the Active Directory database to locate the distinguishedName corresponding to the userid provided in the login attempt, then attempts to verify that the user can bind using their distinguishedName and the password they provided.

 

The Java Extension (version 2)

 

import java.io.IOException;  
import java.io.PrintWriter;  
import java.util.Hashtable;  
  
  
import javax.naming.NamingEnumeration;  
import javax.naming.Context;  
import javax.naming.directory.Attribute;  
import javax.naming.directory.Attributes;  
import javax.naming.directory.DirContext;  
import javax.naming.directory.InitialDirContext;  
import javax.naming.directory.SearchControls;  
import javax.naming.directory.SearchResult;  
  
import javax.servlet.ServletConfig;  
import javax.servlet.ServletException;  
import javax.servlet.http.HttpServlet;  
import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
  
import com.zeus.ZXTMServlet.ZXTMHttpServletRequest;  
  
public class LdapAuthenticate extends HttpServlet {  
    private static final long serialVersionUID = 1L;  
    private String dirServer;  
    private String realm;  
    private String authentication;  
    private String bindDN;  
    private String bindPassword;  
    private String baseDN;  
    private String filter;  
  
    public void init( ServletConfig config) throws ServletException {  
        super.init( config );  
        dirServer = config.getInitParameter( "DB" );  
        if( dirServer == null ) throw new ServletException( "No DB configured" );  
        realm = config.getInitParameter( "Realm" );  
        if( realm == null ) realm = "Secure site";  
        authentication = config.getInitParameter( "authentication" );  
        if( authentication == null ) authentication = "simple";  
        bindDN = config.getInitParameter( "bindDN" );  
        if( dirServer == null ) throw new ServletException( "No bindDN configured" );  
        bindPassword = config.getInitParameter( "bindPassword" );  
        if( dirServer == null ) throw new ServletException( "No bindPassword configured" );  
        baseDN = config.getInitParameter( "baseDN" );  
        if( dirServer == null ) throw new ServletException( "No baseDN configured" );  
        filter = config.getInitParameter( "filter" );  
        if( dirServer == null ) throw new ServletException( "No filter configured" );  
    }  
      
    public void doGet( HttpServletRequest req, HttpServletResponse res )  
        throws ServletException, IOException  
    {  
        try {  
            ZXTMHttpServletRequest zreq = (ZXTMHttpServletRequest)req;  
              
            String[] userPass = zreq.getRemoteUserAndPassword();  
            if( userPass == null ) throw new Exception( "No Authentication details" );  
  
  
            Hashtable<String, String> env = new Hashtable<String, String>();  
            env.put( Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");  
            env.put( Context.PROVIDER_URL, "LDAP://" + dirServer );  
            env.put( Context.SECURITY_AUTHENTICATION, authentication );  
  
            /* Bind with admin credentials */  
              
            NamingEnumeration<SearchResult> ne;  
            String searchfilter = filter.replace( "%u", userPass[0] );  
            try {  
                env.put( Context.SECURITY_PRINCIPAL, bindDN );  
                env.put( Context.SECURITY_CREDENTIALS, bindPassword );  
  
                DirContext ctx = new InitialDirContext( env );  
                String[] attrIDs = { "distinguishedName" };  
                SearchControls sc = new SearchControls();    
                sc.setReturningAttributes( attrIDs );  
                sc.setSearchScope(SearchControls.SUBTREE_SCOPE);    
                ne = ctx.search(baseDN, searchfilter, sc);  
                ctx.close();  
            } catch( Exception e ) {  
                throw new Exception( "Failed to bind with master credentials: " + e.toString() );  
            }  
  
            if( ne == null || !ne.hasMore() ) { throw new Exception( "No such user " + userPass[0] ); }   
              
            SearchResult sr = (SearchResult) ne.next();  
            Attributes attrs = sr.getAttributes();  
            Attribute dnAttr = attrs.get("distinguishedName");  
            String dn = (String) dnAttr.get();  
        
            /* Now bind using dn with the user credentials */  
            try {   
                env.put( Context.SECURITY_PRINCIPAL, dn );  
                env.put( Context.SECURITY_CREDENTIALS, userPass[1] );  
      
                DirContext ctx = new InitialDirContext( env );  
                ctx.close();  
            } catch( Exception e ) {  
                throw new Exception( "Failed to bind with user credentials: " + e.toString() );  
            }  
  
            // No exceptions thrown... must have been successful ;-)  
            return;  
        } catch( Exception e ) {  
            res.setHeader( "WWW-Authenticate", "Basic realm=\"" + realm + "\"" );  
            res.setHeader( "Content-Type", "text/html" );  
            res.setStatus( 401 );  
              
            String message =   
                "<html>" +  
                "<head><title>Unauthorized</title></head>" +  
                "<body>" +  
                "<h2>Unauthorized - please log in</h2>" +  
                "<p>Please log in with your system username and password</p>" +  
                "<p>Error: " + e.toString() + "</p>" +  
                "</body>" +  
                "</html>";  
              
            PrintWriter out = res.getWriter();  
            out.println( message );  
        }  
    }  
  
    public void doPost( HttpServletRequest req, HttpServletResponse res )  
        throws ServletException, IOException  
    {  
        doGet( req, res );  
    }  
}

 

This version of the Java Extension takes additional initialization parameters:

ldapparams2.png

It first searches the database for a distinguishedName using a query resembling:

 

$ ldapsearch -h DB -D bindDN -w bindPassword -b baseDN filter distinguishedName

 

Where the %u in the filter is replaced with the username in the login attempt.

 

It then attempts to bind to the database using a query resembling:

 

$ ldapsearch -h DB -D distinguishedName -w userpassword

 

... and permits access if that bind is successful.

Contributors