cfWheels – Active Directory / LDAP authentication

Adding login and administrative features to your cfWheels apps has cropped up on the mailing list a few times, so I thought I’d just pull together a simple example for Active Directory / LDAP authentication.

In this particular snippet, I want to establish a user a) has credentials on the server, and b) belongs to a group called ‘ContactsDatabaseUsers’.

This method has the benefit of a) not having to store user’s passwords in your application database and b) allowing system administrators to control access to the application.

This example assumes you have a login form at /main/login/ which posts params.username and params.password to /main/dologin/

I also would have a local users model where I could add additional application specific options: i.e, when they last logged in, etc.

This information would then be copied to the session scope. ie. session.currentuser

When the users first login, this local entry needs to be created, or copied to the session scope from the existing entry.

<--------Main.cfc----------->
<cffunction name="init">
<cfscript>
// AD Auth, application wide: if I wanted to only restrict a subset a pages, I could use 'only' instead of except below.
// This filter call should be in every controller you wish to protect
filters(through="loginRequired", except="login,dologin");	
// Login
verifies(only="dologin", post=true, params="username,password", error="Both username and password are required", handler="login");	
</cfscript>
</cffunction>

<cffunction name="dologin" hint="Logs in a user via LDAP"> 
  <cfset var ldap=QueryNew("")/>
  <!--- The LDAP Server to authenticate against--->
  <cfset var server="ldap.server.domain.com">
  <!--- The start point in the LDAP tree --->
  <cfset var start="OU=People,DC=domain,DC=com">
  <!--- Scope --->
  <cfset var scope="subtree">
  <!---- Name of group --->
  <cfset var cn = "ContactsDatabaseUsers">
               <cftry>
                   <!--- Check the user's credentials against Windows Active Directory via LDAP--->
                   <cfldap
                       server="#server#" 
                       username="DOMAIN#params.username#" 
                       password="#params.password#" 
                       action="query" 
                       name="ldap" 
                       start="#start#" 
                       scope="#scope#" 
                       attributes="*" 
                       filter="(&(objectclass=*)(sAMAccountName=#params.username#))"/>
                      	
                       <!--- Get the memberOf result and look for contactsDatabase---> 
                       <cfquery dbtype="query" name="q">
                       SELECT * FROM ldap WHERE name = <cfqueryparam cfsqltype="cf_sql_varchar" value="memberOf">
                       </cfquery>
                                             
                       <cfscript>
		if (q.value CONTAINS "CN=#cn#")
			{
			params.name=params.username;
			// Check for the local user profile
			if( model("user").exists(where="username = '#params.username#'"))
			{
				// User found, copy user object to session scope
				user=model("user").findOne(where="username = '#params.username#'"); 
				user.loggedinAt=now();
				user.save();
				session.currentuser=user;                              
				flashInsert(success="Welcome back!");
			}
			else
			{
				// No Local Entry, create user as LDAP Auth has been successful
				user=model("user").create(username=params.username, name=params.name, loggedinAt=now());
				if (NOT user.hasErrors()){
					user.save();         
					session.currentuser=user;							
					flashInsert(success="Welcome - as this is the first time you've logged in, your account has been created.");
				}
				else
				{
					flashInsert(error="Error creating local account from successful Active Directory authentication");
					StructClear(Session);
					renderPage(action="login");
				}
			}
			redirectTo(route="home");
		}
		else 
		{
		 redirectTo(route="home");
		 }						
      </cfscript> 

   <cfcatch type="any"> 
      <cfscript>
           if(FindNoCase("Authentication failed", CFCATCH.message)){
            // Likely to be user error, e.g. typo, wrong username or password
             flashInsert(error="Login Failed: please check your username and password combination");
             renderPage(action="login");
            }
            else {
            // Likely to be LDAP error, e.g. not working, wrong parameters/attributes
            flashInsert(error="Login Failed: Please try again.");
            renderPage(action="login");
            }
       </cfscript> 
   </cfcatch>
 </cftry>                       
</cffunction>
</cfcomponent>

<-----------Controller.cfc------------->
<cffunction name="loginRequired" hint="This function will prevent non-logged in users from accessing specific actions">
<cfif NOT isloggedin()> 
	<cfset redirectTo(controller="main", action="login")>
</cfif>
</cffunction> 
    
<cffunction name="isloggedin" hint="Checks for existence of session struct">
<cfif NOT StructKeyExists(session, "currentUser")> 
   <cfreturn false>
<cfelse>
   <cfreturn true>
</cfif>
</cffunction>