Storing passwords as a hash value or using an external authenticator

Note: we use MD5 in the example below, and MD5 is still resistant to preimage attacks. See security note below.

The authenticator types are by default designed to provide a unified mechanism for verifying the user's credentials. For example, the default for the callback function provided to ba.create.authuser() is to return the password in plaintext. Similarly, the default storage for the JSON User DB is to store the passwords in plaintext.

The reason the authentication mechanism defaults to using plaintext passwords is to unify the three inherent different authenticator types: Basic, Digest, and Form. The plaintext format enables all authenticators to authenticate the user using the same plaintext API.

In some systems, you may not have a secure method for storing plaintext passwords, or you may be required to authenticate using an external database such as Radius or LDAP. The following provides an alternative method that allows you to either store the password as a hash or use an external authenticator.

Storing the password as a hash value

The Digest Authenticator creates a problem when passwords are not stored in plaintext since digest authentication is internally creating a hash value based on the following: user's username, realm name used by the Digest Authenticator, and user's password. A hash value of the password must therefore conform to the format used by Digest authentication. This format is called HA1 and is calculated as follows:

HA1 = MD5(username ":" realm ":" password)

All authenticators, including Basic, Digest, Form, and Sform, have been designed such that they can accept this format, thus providing a unified API for authenticating users.

The following example shows how the callback function provided to ba.create.authuser() informs the authenticator that a HA1 hash is returned and not a plaintext password.

local function getpassword(username)
   if username == "user" then -- One user only: username "user"
      local realm = "Barracuda Server" -- Must match realm name in authenticator
      local password = "password"
       -- Return HA1 as a 32 byte hex value
      local ha1=ba.crypto.hash"md5"(username)":"(realm)":"(password)(true,"hex")
      -- HA1 enclosed in a Lua table notifies the caller that this is a HA1 hash
      return {ha1}
   -- No return value is the same as false i.e. not authenticated/found

The above password function is designed for one user with the name "user". The HA1 hash is calculated in the callback function and illustrates how the hash is assembled by using function ba.crypto.hash. You would normally pre-calculate the hash and store the hash in persistent storage. The getpassword function would then look up the user and return the pre-calculated hash value.

Notice how we enclose the hash value in a Lua table. The hash is a string; thus we need a mechanism where we tell the authenticator that this is not a plaintext password, but an HA1 hash.

The JSON User DB can also store HA1 hash values. You use the same table encapsulation mechanism to notify that the password is a hash value and not a string.

Plaintext password format HA1 hash format
 pwd={'pre-calculated HA1 enclosed as a table'},

HA1 Security Note:

MD5 is deemed insecure when it comes to collision attacks, but collision attacks cannot be used against HA1 hashes. The type of attack than can be deployed against HA1 is a so called preimage attack, which is still considered infeasible. See the Wikipedia preimage attack article for details.

Using an external authenticator

An external user database, such as Radius or LDAP, will typically provide a yes/no answer and not provide the password in return. This means that the getpassword callback function cannot return the password in plaintext or return the password as an HA1 hash value. The getpassword function can only return true (authenticated) or false (not authenticated). The Basic and Form Authenticators can accept a yes/no answer; however, the Digest Authenticator will not accept a yes/no answer. This means that you will not be able to use digest authentication when selecting this authentication type. Digest authentication is required by some HTTP clients, such as WebDAV clients; thus you may experience problems in advanced setups when you exclude Digest authentication as an option.

The following example shows how the callback function provided to ba.create.authuser() can return a yes/no answer to a Basic or Form Authenticator.

local function getpassword(username, upasswd)
   local isAuthenticated = myLdapServer(username, upasswd)
   -- isAuthenticated is a boolean value
   return isAuthenticated

Notice the extra parameter 'upasswd'. This is the password provided by the user. We send the two variables provided by the user, attempting to authenticate, to function "myLdapServer", a fictitious function that returns true or false. The boolean value is returned to the authenticator.