Implementing user security

The following section provide several examples of ways to implement security using basic authentication and application authentication

Basic authentication user security example

The example in this section shows how you might implement user security using web-server basic authentication and two roles, user and administrator.

This example has two ColdFusion pages:

You can test the security behavior by adding your own pages to the same directory as the Application.cfm page.

Example: Application.cfm

The Application.cfm page consists of the following:

<cfapplication name="Orders">
<cflogin>
  <cfif IsDefined( "cflogin" )>
    <cfif cflogin.name eq "admin">
      <cfset roles = "user,admin">
    <cfelse>
        <cfset roles = "user">
      </cfif>
    <cfloginuser name = "#cflogin.name#" password = "#cflogin.password#"
      roles = "#roles#" />
  <cfelse>
    <!--- this should never happen --->
    <h4>Authentication data is missing.</h4>
      Try to reload the page or contact the site administrator.
  <cfabort>
  </cfif>
</cflogin>

Reviewing the code

The Application.cfm page executes before the code in each ColdFusion page in an application. For more information on the Application.cfm page and when it is executed, see Chapter 13, "Designing and Optimizing a ColdFusion Application".

The following table describes the CFML code in Application.cfm and its function:
Code
Description
<cfapplication name="Orders"
Identifies the application. The login information on this page only applies to this application.
<cflogin>
  <cfif IsDefined( "cflogin" )>
    <cfif cflogin.name eq "admin">
      <cfset roles = "user,admin">
    <cfelse>
        <cfset roles = "user">
      </cfif>
Executes if there is no logged-in user.
Makes sure the user is correctly logged in by the web-server. (Otherwise there would be no cflogin variable.)
Sets a roles variable based on the user's ID. assigns users named "admin" to the admin role. Assigns all other users to the users role.
<cfloginuser name = "#cflogin.name#"
  password = "#cflogin.password#"
  roles = "#roles#" />
Logs the user into the ColdFusion security system and specifies the user's password, name and roles. Gets the password and name directly from the cflogin structure.
<cfelse>
  <!--- this should never happen --->
  <h4>Authentication data is missing.</h4>
    Try to reload the page or contact the
      site administrator.
  <cfabort>
This code should never run, but if the user somehow got to this page without logging in to the web server, this message would display and ColdFusion would stop processing the request.
  </cfif>
</cflogin>
Ends if/else block.
Ends the cflogin tag body.

Example: securitytest.cfm

The securitytest.cfm page shows how any application page can use ColdFusion user authorization features. The web server ensures the existence of an authenticated user, and the Application.cfm page ensures that the user is assigned to roles the page content appears. The securitytest.cfm page uses the IsUserInRole and GetAuthUser functions to control the information that is displayed.

The securitytest.cfm page consists of the following code:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
  <title>Basic authentication security test page</title>
</head>

<body>
<cfoutput>
  <h2>Welcome #GetAuthUser()#!</h2>
</cfoutput>

ALL Logged-in Users see this message.<br>
<br>
<cfscript>
  if (IsUserInRole("admin"))
    WriteOutput("users in the admin role see this message.<br><br>");
  if (IsUserInRole("user"))
    WriteOutput("Everyone in the user role sees this message.<br><br>");
</cfscript>

</body>
</html>

Reviewing the code

The following table describes the securitytest.cfm page CFML code and its function:
Code
Description
<cfoutput>
<h2>Welcome #GetAuthUser()#!</h2>
</cfoutput>
Displays a welcome message that includes the user's login ID.
ALL Logged-in Users see this   message.<br>
<br>
Displays this message in all cases. The page does not display until a user is logged in.
<cfscript>
  if (IsUserInRole("admin"))
    WriteOutput("users in the admin role
      see this message.<br><br>");
  if (IsUserInRole("user"))
    WriteOutput("Everyone in the user role
      sees this message.<br><br>");
</cfscript>
Tests whether the user belongs to each of the valid roles. If the user is in a role, displays a message with the role name.
The user sees one message per role to which he or she belongs.

Application-based user security example

The example in this section shows how you might implement user security by authenticating users and then allowing users to see or use only the resources that they are authorized to access.

This example has three ColdFusion pages:

You can test the security behavior by adding your own pages to the same directory as the Application.cfm page.

The example gets user information from the LoginInfo table of the CompanyInfo database that is installed with ColdFusion. You can replace this database with any database containing UserID, Password, and Roles fields. The sample database contains the following data:
UserID
Password
Roles
BobZ
Ads10
Employee,Sales
JaniceF
Qwer12
Contractor,Documentation
RandalQ
ImMe
Employee,Human Resources,Manager

Because spaces are meaningful in roles strings, you should not follow the comma separators in the Roles fields with spaces.

The following sections contain listings and descriptions of each of the pages.

Example: Application.cfm

The Application.cfm page consists of the following:

<cfapplication name="Orders" sessionmanagement="Yes">
<cfif IsDefined("Form.logout")>
  <cflogout>
</cfif>

<cflogin>
  <cfif NOT IsDefined("cflogin")>
    <cfinclude template="loginform.cfm">
    <cfabort>
  <cfelse>
    <cfif cflogin.name IS "" OR cflogin.password IS "">
      <cfoutput>
        <H2>You must enter text in both the User Name and Password fields</H2>
      </cfoutput>
      <cfinclude template="loginform.cfm">
      <cfabort>
    <cfelse>
      <cfquery name="loginQuery" dataSource="CompanyInfo">
      SELECT UserID, Roles
      FROM LoginInfo
      WHERE
        UserID = '#cflogin.name#'
        AND Password = '#cflogin.password#'
      </cfquery>
      <cfif loginQuery.Roles NEQ "">
        <cfloginuser name="#cflogin.name#" Password = "#cflogin.password#"
          roles="#loginQuery.Roles#">
      <cfelse>
        <cfoutput>
          <H2>Your login information is not valid.<br>
          Please Try again</H2>
        </cfoutput>  
        <cfinclude template="loginform.cfm">
        <cfabort>
      </cfif>
    </cfif>  
  </cfif>
</cflogin>


<cfif GetAuthUser() NEQ "">
  <cfoutput>
     <form action=MyApp/index.cfm" method="Post">
      <input type="submit" Name="Logout" value="Logout">
    </form>
  </cfoutput>
</cfif>

Reviewing the code

The Application.cfm page executes before the code in each ColdFusion page in an application. For more information on the Application.cfm page and when it is executed, see Chapter 13, "Designing and Optimizing a ColdFusion Application". The following table describes the CFML code in Application.cfm and its function:.
Code
Description
<cfapplication name="Orders"
  sessionmanagement="Yes">

Identifies the application and enables Session scope variables.
<cfif IsDefined("Form.logout")>
  <cflogout>
</cfif>
If the user just submitted the logout form, logs out the user. The following cflogin tag runs as a result.
<cflogin>
  <cfif NOT IsDefined("cflogin")>
    <cfinclude template="loginform.cfm">
    <cfabort>

Executes if there is no logged-in user.
Tests to see if the user has submitted a login form. If not, uses cfinclude to display the form. Uses the built-in cflogin variable that contains the user name and password if it was submitted by the login form.
The cfabort tag prevents processing of any code that follows on this page.
<cfelse>
  <cfif cflogin.name IS "" OR
    cflogin.password IS "">
    <cfoutput>
      <H2>You must enter text in both the
      User Name and Password fields</H2>
    </cfoutput>
    <cfinclude template="loginform.cfm">
    <cfabort>
Executes if the user submitted a login form.
Tests to make sure both name and password have data. If either variable is empty, displays a message, followed by the login form.
The cfabort tag prevents processing of any code that follows on this page.
<cfelse>
  <cfquery name="loginQuery"
      dataSource="CompanyInfo">
    SELECT UserID, Roles
    FROM LoginInfo
    WHERE
      UserID = '#cflogin.name#'
      AND Password = '#cflogin.password#'
  </cfquery>
Executes if the user submitted a login form and both fields contain data.
Uses the cflogin structure's name and password entries to find the user record in the database and get the user's roles.
<cfif loginQuery.Roles NEQ "">
  <cfloginuser name="#cflogin.name#"
    Password = "#cflogin.password#"
    roles="#loginQuery.Roles#">
If the query returns data in the Roles field, logs in the user using the UserID and Roles fields from the database. In this application, every user must be in some role.
<cfelse>
  <cfoutput>
    <H2>Your login information is not
      valid.<br>
    Please Try again</H2>
  </cfoutput>  
  <cfinclude template="loginform.cfm">
  <cfabort>
Executes if the query did not return a role. If the database is valid, this means there was no entry matching the user ID and password. Displays a message, followed by the login form.
The cfabort tag prevents processing of any code that follows on this page.
      </cfif>
    </cfif>  
  </cfif>
</cflogin>
Ends loginquery.Roles test code.
Ends form entry empty value test.
Ends form entry existence test.
Ends cflogin tag body.
<cfif GetAuthUser() NEQ "">
  <cfoutput>
    <form action=MyApp/index.cfm"
        method="Post">
      <input type="submit" Name="Logout"
        value="Logout">
    </form>
  </cfoutput>
</cfif>
If a user is logged in, displays the Logout button.
If the user clicks the button, posts the form to the application's (theoretical) entry page, index.cfm.
Application.cfm then logs out the user and displays the login form. If the user logs in again, ColdFusion displays index.cfm.

Example: loginform.cfm

The loginform.cfm page consists of the following:

<cfset url="http://" & "#CGI.server_name#" & ":" &"#CGI.server_port#" &
   "#CGI.script_name#">
<cfif CGI.query_string IS NOT "">
  <cfset url=url & "?#CGI.query_string#">
</cfif>
<H2>Please Log In</H2>
<cfoutput>
  <form action="#url#" method="Post">
    <table>
      <tr>
        <td>username:</td>
        <td><input type="text" name="j_username"></td>
      </tr>
      <tr>
        <td>password:</td>
        <td><input type="password" name="j_password"></td>
      </tr>
    </table>
    <br>
    <input type="submit" value="Log In">
  </form>
</cfoutput>

Reviewing the code

The following table describes the loginform.cfm page CFML code and its function:
Code
Description
<cfset url="http://" &
  "#CGI.server_name#" & ":" &
  "#CGI.server_port#" &
  "#CGI.script_name#">
<cfif CGI.query_string IS NOT "">
  <cfset url=url &
    "?#CGI.query_string#">
</cfif>
Constructs the URL to use in the form's action attribute from CGI variables. The CGI.script_name variable identifies the originally requested page, because that page's invocation of Application.cfm causes the login form to be displayed.
Appends any URL query string used in requesting the page so the page can process the request.
A more secure application would use the HTTPS protocol to encrypt the user name and password. To use HTTPS, you must obtain a security certificate and enable the protocol in your web server.
<H2>Please Log In</H2>
<cfoutput>
  <form action="#url#" method="Post">
    <table>
      <tr>
        <td>username:</td>
        <td><input type="text"
          name="j_username"></td>
      </tr>
      <tr>
        <td>password:</td>
        <td><input type="password"
          name="j_password"></td>
      </tr>
    </table>
    <br>
    <input type="submit" value="Login">
  </form>
</cfoutput>
Displays the login form. The form requests a user ID and password and posts the user's input to the page specified by the url variable.
Uses the field names j_username and j_password. ColdFusion automatically puts form fields with these values in the cflogin.name and cflogin.password variables inside the cflogin tag.

Example: securitytest.cfm

The securitytest.cfm page shows how any application page can use ColdFusion user authorization features. Application.cfm ensures the existence of an authenticated user before the page content appears. The securitytest.cfm page uses the IsUserInRole and GetAuthUser functions to control the information that is displayed.

The securitytest.cfm page consists of the following code:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
  <title>Security test page</title>
</head>

<body>
<cfoutput>
  <h2>Welcome #GetAuthUser()#!</h2>
</cfoutput>

ALL Logged-in Users see this message.<br>
<br>
<cfscript>
  if (IsUserInRole("Human Resources"))
    WriteOutput("Human Resources members see this message.<br><br>");
  if (IsUserInRole("Documentation"))
    WriteOutput("Documentation members see this message.<br><br>");
  if (IsUserInRole("Sales"))
    WriteOutput("Sales members see this message.<br><br>");
  if (IsUserInRole("Manager"))
    WriteOutput("Managers see this message.<br><br>");
  if (IsUserInRole("Employee"))
    WriteOutput("Employees see this message.<br><br>");
  if (IsUserInRole("Contractor"))
    WriteOutput("Contractors see this message.<br><br>");
</cfscript>

</body>
</html>

Reviewing the code

The following table describes the securitytest.cfm page CFML code and its function:
Code
Description
<cfoutput>
<h2>Welcome #GetAuthUser()#!</h2>
</cfoutput>
Displays a welcome message that includes the user's login ID.
ALL Logged-in Users see this   message.<br>
<br>
Displays this message in all cases. The page does not display until a user is logged in.
<cfscript>
  if (IsUserInRole("Human Resources"))
    WriteOutput("Human Resources members
    see this message.<br><br>");
  if (IsUserInRole("Documentation"))
    WriteOutput("Documentation members see
    this message.<br><br>");
  if (IsUserInRole("Sales"))
    WriteOutput("Sales members see this
    message.<br><br>");
  if (IsUserInRole("Manager"))
    WriteOutput("Managers see this
    message.<br><br>");
  if (IsUserInRole("Employee"))
    WriteOutput("Employees see this
    message.<br><br>");
  if (IsUserInRole("Contractor"))
    WriteOutput("Contractors see this
    message.<br><br>");
</cfscript>
Tests whether the user belongs to each of the valid roles. If the user is in a role, displays a message with the role name.
The user sees one message per role to which he or she belongs.

Using application-based security with a browser's login dialog

You do not have to create a login page to display a user login form; you can rely on the browser to display its standard login page. To do so, your cflogin tag body returns an HTTP status 401 to the browser if the user is not logged in or if the login fails. The browser then displays its login page and returns the information to the cflogin tag's cflogin structure when the user clicks the login button.

For example, the following code tells the browser to display a login form if the user has not logged in, or if the user does not provide a name "user" and password "p1", or a name "admin" and password "p2":

<cflogin>
  <cfif IsDefined( "cflogin" )>
    <cfif cflogin.name eq "admin" and cflogin.password eq "p1">
      <cfset roles = "user,admin">
    <cfelseif cflogin.name eq "user" and cflogin.password eq "p2">
      <cfset roles = "user">
    </cfif>
  </cfif>
  
  <cfif IsDefined( "roles" )>
    <cfloginuser name="#cflogin.name#" password="#cflogin.password#" roles="#roles#">
  <cfelse>
    <!--- User has not logged in or authentication failed - send 401 --->
    <cfsetting enablecfoutputonly="yes" showdebugoutput="no">
    <cfheader statuscode="401">
    <cfheader name="WWW-Authenticate" value="Basic realm=""MySecurity""">
    <cfoutput>Not authorized</cfoutput>
    <cfabort>
    
  </cfif>
</cflogin>

Using an LDAP Directory for security information

LDAP directories are often used to store security information. The following example cflogin tag checks an LDAP directory to authenticate the user and retrieve the users roles.

The most important thing to note in this example is that it queries the directory twice, first as the directory manager, then with the user's identity:

The "Reviewing the code" section that follows describes the code's function in detail. For more information on using LDAP directories with ColdFusion, see Chapter 23, "Managing LDAP Directories".

<cflogin>
<!--- setting basic attributes ---> 
<cfset root = "o=macromedia.com"> 
<cfset server="ldap.macromedia.com"> 
<cfset port="389"> 

<!--- These attributes are used in the first search. --->
<!--- This filter will look in the objectclass for the user's ID. ---> 
<cfset filter = "(&(objectclass=*)(uid=#Form.UserID#))"> 
<!--- Need directory manager's cn and password to get the user's 
    password from the directory ---> 
<cfset LDAP_username = "cn=directory manager"> 
<cfset LDAP_password = "password"> 

<!--- Search for the user's dn information. This is used later to 
    authenticate the user.
      NOTE: Do this as the Directory Manager to ensure access to the 
    information ---> 
<cftry>
  <cfldap action="QUERY"
      name="userSearch"
      attributes="uid,dn"
      start="#root#"
      scope="SUBTREE"
      server="#server#"
      port="#port#"
      filter="#filter#"
      username="#LDAP_username#"
      password="#LDAP_password#"
  > 
  <cfcatch type="Any">
    <cfset UserSearchFailed = true>
  </cfcatch>
</cftry>
<!--- If user search failed or returns 0 rows abort --->
<cfif NOT userSearch.recordcount OR UserSearchFailed>
  <cfoutput>
    <script> alert("UID for #uid# not found"); </script>
   </cfoutput>
   <cfabort>
</cfif>

<!--- pass the user's DN and password to see if the user authenticates 
    and get the user's roles>

<cftry>
  <cfldap 
    action="QUERY"
    name="auth"
    attributes="dn,roles"
    start="#root#"
    scope="SUBTREE"
    server="#server#"
    port="#port#"
    filter="#filter#"
    username="#userSearch.dn#"
    password="#Form.password#"
  >

  <cfcatch type="any">
    <cfif FindNoCase("Invalid credentials", cfcatch.detail)>
      <cfoutput><script>alert("User ID or Password invalid for user:
#Form.userID#")</script>
      </cfoutput>
      <cfabort>
    <cfelse>
      <cfoutput><script>alert("Unknown error for user: #Form.userID#
#cfcatch.detail#")</script>
      </cfoutput>
      <cfabort>
    </cfif>
  </cfcatch>
</cftry>  

<!--- If the LDAP query returned a record, the user is valid. --->
<cfif auth.recordcount>
  <cfloginuser name="#Form.userID#" password="#Form.password#"
roles="#auth.roles#">
</cfif>
</cflogin>

Reviewing the code

The following table describes the code and its function:
Code
Description
<cflogin>
<cfset root = "o=macromedia.com"> 
<cfset server="ldap.macromedia.com"> 
<cfset port="389"> 
<cfset filter = "(&(objectclass=*)
  (uid=#Form.UserID#))"> 
<cfset LDAP_username = 
  "cn=directory manager"> 
<cfset LDAP_password = "password"> 
Starts cflogin tag body. Sets several of the values used as attributes in the cfldap tags as variables. This ensures that the same value is used in both tags, and makes it easier to change the settings if needed.
Sets the directory manager's user name and password for the first query.
<cftry>
  <cfldap action="QUERY"
    name="userSearch"
    attributes="uid,dn"
    start="#root#"
    scope="SUBTREE"
    server="#server#"
    port="#port#"
    filter="#filter#"
    username="#LDAP_username#"
    password="#LDAP_password#"
  > 
In a cftry block, uses the directory manager's identity to get the distinguished name (dn) for the user. If the user ID is not in the directory, returns an empty record set.
  <cfcatch type="Any">
    <cfset UserSearchFailed = true>
  </cfcatch>
</cftry>
Catches any exception. Sets a UserSearchFailed flag to True.
Ends the cftry block.
<cfif NOT userSearch.recordcount OR
    UserSearchFailed>
  <cfoutput>
    <script> 
      alert("UID for #uid# not found");
    </script>
   </cfoutput>
   <cfabort>
</cfif>
If the LDAP lookup did not return any results or the UserSearchFailed flag is True, displays an error message and ends processing of the page
<cftry>
  <cfldap 
    action="QUERY"
    name="auth"
    attributes="dn,roles"
    start="#root#"
    scope="SUBTREE"
    server="#server#"
    port="#port#"
    filter="#filter#"
    username="#userSearch.dn#"
    password="#Form.password#">    
In a try block, uses the distinguished name from the previous query and the user-supplied password to access the directory and get the user's roles. If either the dn or password is invalid, the cfldap tag throws an error, which is caught in the cfcatch block.
<cfcatch type="any">
  <cfif FindNoCase("Invalid credentials",
    cfcatch.detail)>
    <cfoutput><script>alert("User ID or
      Password invalid for user:
      #Form.userID#")</script>
    </cfoutput>
    <cfabort>
  <cfelse>
    <cfoutput><script>alert("Unknown error for
      user: #Form.userID# #cfcatch.detail#")
      </script></cfoutput>
    <cfabort>
  </cfif>
</cfcatch>
</cftry>  
Catches any exceptions.
Tests to see if the error information includes the string "invalid credentials", which indicates that either the dn or password is invalid. If so, displays an error message indicating the problem. Otherwise, displays a general error message.
If an error is caught, the cfabort tag ends processing of the request after displaying the error description.
<cfif auth.recordcount>
  <cfloginuser name="#Form.userID#"
    password="#Form.password#"
  roles="#auth.roles#">
</cfif>
</cflogin>
If the second query returned a valid record, logs in the user and sets the roles to the values returned by the query.
Ends the cflogin tag body.

Comments