NOTE As with all of the modules in ESAPI4CF, Authentication is not required for you to implement the library. You can implement any one or more of the modules. However, let me say that without authentication your security logging will lack an accountName and IP address tied to each event which will make all of that nice logging pretty worthless. So I would highly recommend implementing the Authenticator Module but the final decision is all yours.

In this tutorial we are going to cover the Authenticator Module in ESAPI4CF. We will be diving into the what the login process does under the hood, adding onto our Application.cfc that we defined in the Setup Tutorial in order to authenticate users, going over the other functionality provided by this module, and lastly covering how to implement your own user base into ESAPI4CF.

All of the modules are accessible via our main ESAPI instance:

application.ESAPI.authenticator()

At some point in your application you will have to create users and you will have certain business logic in order to do so. But what type of validation do you have that the accountName and password being created are in fact strong enough? Well luckily the Authenticator Module has got you covered. There is a method call included which will validate the accountName and password to ensure their strength.

application.ESAPI.authenticator().createUser(accountName, password1, password2)

The accountName will be verified for uniqueness and validated against a regular expression that you can customize in the ESAPI.properties. In many applications the email address is also used as the accountName so if that is the case this regular expression should be the same as the email regular expression you configure in ESAPI.properties.

Validator.AccountName=^[a-zA-Z0-9]{3,20}$

The password is also asked for twice to encourage user interfaces to require the re-type password field and the two passwords here will be verified as matching.

The password strength will also be verified. It is important to understand the password complexity rules that ESAPI4CF provides. The password complexity works off of four character sets:

Below are the password complexity rules that must be met in order to pass validation:

If you require that a random password be generated instead of the user providing one, ESAPI4CF can take care of generating a random secure password for you and you can pass this to the createUser method. The password will contain letters, numbers, and special characters.

application.ESAPI.authenticator().generateStrongPassword()

At any point you need to retrieve one of your users there is a built-in method to accomplish just this.

application.ESAPI.authenticator().getUserByAccountId(accountId)

But we don't always have the accountId available so we can also do this by accountName.

application.ESAPI.authenticator().getUserByAccountName(accountName)

We also run into scenario's where we need to know who our current user is using the web application. Regardless of whether your application currently has an authenticated user, an anonymous user, or just don't know which user you have you can retrieve who the current user is at anytime. Both the authenticated user and anonymous user instances implement the same User interface so you can rest assured they contain the same methods.

application.ESAPI.authenticator().getCurrentUser()

So all this talk about users within ESAPI4CF, what exactly does this user instance look like? The user is pretty basic so there is not really a point in going through them all. You can view the methods in the User module.

Changing a password follows a similar pattern as createUser where the new password is asked for twice.

application.ESAPI.authenticator().changePassword(user, currentPassword, newPassword, newPassword2)

The same type of validation is performed around your password - matching and complexity. Plus the new password will be checked against the passwordHash history. The passwordHash is not permitted to match any previous passwordHashes in the history. The number of passwordHashes in the history is configurable in the ESAPI.properties file.

MaxOldPasswordHashes=13

You can also call the changePassword directly on the current user.

application.ESAPI.authenticator().getCurrentUser().changePassword(oldPassword, newPassword1, newPassword2)

In many cases you will want to have the user verify their password again when they access or update sensitive data like email address, accountName, or other personal information. This can be accomplished with the verifyPassword method.

application.ESAPI.authenticator().verifyPassword(user, password)

Or directly on the current user.

application.ESAPI.authenticator().getCurrentUser().verifyPassword(password)

ESAPI4CF comes out of the box using double-salted, SHA-512 algorithm, 1024 iterations hashing of passwords. The first salt we mentioned in the Setup Tutorial when we covered MasterSalt. The second salt is the accountName of the user. The advantage of double salting is that if someone got ahold of your user database, they would not have the MasterSalt since this is stored elsewhere.

As for the algorithm and iterations they are configurable in your ESAPI.properties file. Be aware that once you have established users, changing these values will invalidate their passwordHashes.

HashAlgorithm=SHA-512
HashIterations=1024

IMPORTANT Because the accountName is used in the passwordHash this means that when the user changes their accountName you must also require them to verify their password so that you have both pieces of information in order to reset the passwordHash.

So how does the current user get authenticated anyway? Let us go over what I would say is the single most important method in entire ESAPI4CF library, the login method. This is obvious but I will say it anyway: the login method is used to authenticate your user for your request. I stress the word request here because we login the user on every request not just for the session. Say what? This naming of this method is decieving as it really should be named loginAndPersistAndValidateUser because that is really what it does. In addition to logging our user in via the traditional username and password it also logs our user in via the RememberToken, takes care of persisting/retrieving our user across requests, and checks the various constraints put on our users like expiration, lockout, activation, etc. And by the way if anything fails along the way, an Authentication exception is thrown so you can easily catch this. Can we say WOW? So let's get through any confusion by jumping into the details.

In order to authenticate a user, which will also return the ESAPI4CF user, we make this call per request:

application.ESAPI.authenticator().login(application.ESAPI.currentRequest(), application.ESAPI.currentResponse())

So I am sure this single call brings up several questions, all of which will be answered.

  1. How do you login without an accountName and password?
  2. What's with those two arguments?
  3. What auto-magic is this?

The answer to the first two questions is related so I'll answer them together. As I stated earlier we make this call on every request. Well on every request we will not have an accountName and password, we only have those when an actual login attempt is made. Instead we pass the safe request and safe response instances that we registered with ESAPI4CF back in the Setup Tutorial. As you may know these instances contain all of the information around the request and the desired response respectively, like:

So as you can see this login method along with these two arguments will handle the entire life of authentication including the persistence, not just the login itself. So what's the magic?

The Magic

Here we are going to step through the default authentication process step-by-step. When you make the above call to the login method here is what occurs:

1. Validates the request and response arguments

We need to first ensure that the arguments provided are in fact valid in order to proceed.

2. Checks the session scope for a persisted user

Here we verify the session is valid and check whether we have a persisted user here. If a persisted user is found we can proceed to Step 5.

Most requests will fall under this scenario as this is where ESAPI4CF stores the persisted user.

3. Checks for a RememberToken cookie

The RememberToken cookie is checked for existence and is attempted to be unsealed. The seal/unseal process is part of the Encryptor module but the short explanation is that the unseal process decrypts the data and checks that the timestamp has not expired. If data was able to be unsealed, it verifies the user exists then attempts to log that user in with their password. If all goes well, proceed to Step 5.

IMPORTANT Implementing a RememberToken is optional and is not on by default in ESAPI4CF. If you choose to set the RememberToken, which is in the HTTPUtilities module, the login method is already set up to check for it.

You can also configure how many days the RememberToken persists in the user's cookie in the ESAPI.properties.

RememberTokenDuration=14

4. Checks for login attempt

The HTTP headers are checked to see if the username and password parameters exist and retrieves their values. The actual name of these two parameters are configurable in the ESAPI.properties.

UsernameParameterName=username
PasswordParameterName=password

We then check to be sure the username and password values are not empty then moving onto verifying the user exists and finally attempting to log the user in with the provided password. If this is successful move onto Step 5.

5. Verifies a secure request

Any login attempt must be an HTTP POST over SSL. Any request made post-login must be secured over SSL in order to protect the authenitcated session. We cannot proceed unless we are secure.

6. Verify validity of the user

At this point we are validating that our authenticated user is in fact allowed to proceed. Keep in mind that these checks are ran on each request so even an already logged in user could fail the checks below and get logged out. There are several checks to validate our user:

  1. Not Anonymous
  2. Not Disabled
  3. Not Locked
  4. Not Expired
  5. Not exceeded session idle timeout
  6. Not exceeded session absolute timeout

Regarding the two session timeouts, these can be configured in your ESAPI.properties. The idle timeout is equivelant to the ColdFusion sessionTimeout configured in your Application.cfc and these two values should be kept in sync. The absolute timeout is the longest a user is allowed to persist their session. This means that even if the user is active during the entire duration of their session, they will still be logged out once this duration is exceeded. Both of these values are in minutes.

IdleTimeoutDuration=20
AbsoluteTimeoutDuration=120

If at any point during the process the user is established but fails to be validated due to an invalid password, the user is disabled, the user is locked, or the user is expired this will count against the user's login attempts. If the threshold of allowed login attempts is exceeded the user's account will automatically lock to prevent continued unauthorized attempts. The login attempt threshold is configurable in ESAPI.properties. The failed login count is reset upon a successful login. There is no automatic unlocking mechanism so you must build a way to unlock a user in your application.

AllowedLoginAttempts=3

7. Success

If your user made it this far they have successfully passed validation and is considered a valid, authenticated user. You should keep in mind that this is the default implementation of the Authenticator Module and with all of the ESAPI4CF modules it can be overridden with your own implementiation. You will want to do this if you need to also include SSO or API authentication into the login method. You will certainly have to override this module in order to implement your own user base. More on how to use your own implementations will be at the end of this tutorial.

To log the current user out, simply call:

application.ESAPI.authenticator().logout()

This will destroy the RememberToken cookie, invalidate the session, and destroy the JSESSIONID cookie. Changing your sessionId is good security practice when toggling between an authenticated user and anonymous user or vice versa.

So now let's take what we just learned and apply it to our sample app we started in the Setup Tutorial. Do you remember how we left a spot in our onRequestStart method to authentication? Well now it's time to fill that void.

One thing I would like to point out is the use of 3 separate catch statements. Both the AuthenticationCredentialsException and AuthenticationLoginException exceptions extend AuthenticationException so in other languages, like Java and in ESAPI4J, you only have to catch the AuthenticationException exception and all extended types were also caught. CFML does not seem to work this way and treats an exception type explicitly as that defined type; it does not recognize any extended objects. This is unfortunate but as you'll see below it is easy to workaround by having separate catches.

function onRequestStart() {
try {
// register request and response in ESAPI4CF
application.ESAPI.httpUtilities().setCurrentHTTP(getPageContext().getRequest(), getPageContext().getResponse());

// get references to the registered request/response safe wrappers
var httpRequest = application.ESAPI.currentRequest();
var httpResponse = application.ESAPI.currentResponse();

// validate the current request to ensure nothing is suspicious
application.ESAPI.validator().assertIsValidHTTPRequest();

try {
	// this will verify authentication for your entire web application
	// rememberToken is not implemented by default; if you wish to use rememberToken,
	// you must call it inside your login() method after the user has been verified
	application.ESAPI.authenticator().login(httpRequest, httpResponse);
}
catch(org.owasp.esapi.errors.AuthenticationException e) {
	// Possible exceptions:
	// Attempt to login with an insecure request : Received non-SSL request
	// Attempt to login with an insecure request : Received request using GET when only POST is allowed
	// Attempt to access secure content with an insecure request : Received non-SSL request
	redirectToLogin(e);
}
catch(org.owasp.esapi.errors.AuthenticationCredentialsException e) {
	// Possible exceptions:
	// Invalid request : Request or response objects were empty
	// Authentication failed : blank username/password
	// Authentication failed : username does not exist
	redirectToLogin(e);
}
catch(org.owasp.esapi.errors.AuthenticationLoginException e) {
	// Possible exceptions:
	// Login failed : Missing password
	// Login failed : Disabled user attempt to login
	// Login failed : Locked user attempt to login
	// Login failed : Expired user attempt to login
	// Login failed : Incorrect password provided
	// Login failed : Anonymous user cannot be set to current user
	// Login failed : Disabled user cannot be set to current user
	// Login failed : Locked user cannot be set to current user
	// Login failed : Expired user cannot be set to current user
	// Login failed : Session inactivity timeout
	// Login failed : Session absolute timeout
	redirectToLogin(e);
}

// log this request, obfuscating any parameter named password
application.ESAPI.httpUtilities().logHTTPRequest(httpRequest, application.logger, application.ignoredByLogger);
}
catch(Any e) {
application.logger.error(application.ESAPI4JLogger.SECURITY_FAILURE, false, "Error in ESAPI security filter: " & e.message, e);
// let's rethrow this error so your global error handler catches it if you have one
throw(e.message, e.type, e.detail);
}

...other code...

}
	

Since we have 3 separate catches which need to perform the same action, fail authentication, we are going to send them all off to a single method to handle this.

Take notice that we call encodeForURL for each parameter we wish to pass via the URL then to keep these extra safe from URL tampering we will encrypt the parameters and pass this via a single URL encoded parameter. Encoding the encrpyted string is very important in ColdFusion since without the encoding CF chokes on the parameter. Railo on the other hand automatically encodes the encrypted string without the explicit call. So to be safe your best bet is to just encode the encrypted string.

function redirectToLogin(required ex) {
var encoder = application.ESAPI.encoder();
var httpUtilities = application.ESAPI.httpUtilities();

// the ESAPI4CF login exception was already logged so we do not have to do anything with the message/detail
// unless you have specific business requirements to do so.

// let's provide a whitelist of pages that do not require authentication (this is basic, use a better solution)
if (listFindNoCase("login.cfm", listLast(cgi.script_name, "/"))) {
return;
}

// we were not in the whitelist so we must fail this request
var params = "redirect=" & encoder.encodeForURL(cgi.script_name);
params &= "&message=" & encoder.encodeForURL(ex.message);

location(addtoken=false, url="login.cfm?x=" & encoder.encodeForURL(httpUtilities.encryptQueryString(params)));
}

Now on your login.cfm page you will simply have to call decryptQueryString() in order to retrieve this data into a nice structure for your use.

urlX = {};
try {
urlX = application.ESAPI.httpUtilities().decryptQueryString(url.x);
}
catch (org.owasp.esapi.errors.EncryptionException e) {}
catch(expression e) {}

Coming Soon! Instructions on how to implement your own authenticator and/or user base.