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.
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} end -- No return value is the same as false i.e. not authenticated/found end
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='password', roles={'role1','rol2','etc'}, } |
{ pwd={'pre-calculated HA1 enclosed as a table'}, roles={'role1','rol2','etc'}, } |
Although MD5 is considered insecure due to collision attacks, these attacks are not applicable to HA1 hashes. The relevant attack against HA1 would be a preimage attack, which remains computationally infeasible. For more details, see the Wikipedia article on preimage attacks. However, note that all hashing algorithms - including MD5, SHA1, and SHA2 - are susceptible to brute-force attacks. Additionally, if an adversary gains access to HA1 hashes and the server enables HTTP Digest Authentication, these hashes can be used to impersonate users without knowing their actual passwords. Therefore, we recommend storing credentials in the Trusted Platform Module user database.
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 end
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.