This module models a "User" class,
with login credentials,
and Roles Based Access Control.
Additionally,
IP whitelists may be used as an additional validation measure.
Domain (locality) based access control is also provided independently of role based access control.

A brief description of authentication: Passphrases are stored as randomly salted SHA2-512 hashes.
Optional whitelisting of IP's is also available.

User passphrases are salted with a 512 bit random salt (unique per user) using a cryptographically strong random number generator, and converted to a SHA2-512 digest before being stored in the database. All subsequent passphrase validation checks test against the salt and passphrase SHA2 hash.

IP whitelists may be maintained per user. If a user is set to require an IP check, then the user validates only if his passphrase authenticates AND his IP is found in the whitelist associated with his user id.

Users may be given a role, which is conceptually similar to a Unix 'group'. Roles are simple strings. Furthermore, multiple privileges (also simple strings) are granted to roles.

Users may be given multiple domains, which might be used to model localities or jurisdictions. Domains act independently from roles and privileges, but are a convenient way of constraining a role and its privileges to a specific set of localities.

This method creates a new user in the database with the userid supplied when the user object was instantiated. The password field is the only required field. It must contain a clear-text passphrase. There is no length limitation.

Other fields are optional, but convenient. If IP whitelisting is needed for this user, the ip_req field must be supplied, and must be set to 1 (true).

If ip_req is set to 1 (true), a list of valid IP's may also be provided in an arrayref keyed off of ips_aref. As a convenience, the ips key is synonymous with ips_aref. The IP's provided will then be added to the user_ips database table. If an IP is required but none are added via add_user, they will have to be added manually with add_ips before the user can be validated.

The user's passphrase will be salted with a cryptographically sound random salt of 512 bits (128 hex digits). It will then be digested using a SHA2-512 hash, and both the salt and the digest will be stored in the users database.

This is a reliable and secure means of storing a passphrase. In fact, the passphrase is not stored at all. Just a salt and the digest. Even if the salt and hash were to be discovered by an attacker, they would not be useful in side-stepping user validation, as they cannot be used to decrypt the passphrase. SHA512 is the strongest of the SHA2 family. A salt length of 512 bits guarantees a maximum entropy for any given passphrase.

Though it is beyond the scope of this module to enforce, users should be encouraged to use passphrases that are both resistant to dictionary attacks, and dissimilar to passphrases used in other applications. No minimum passphrase size is enforced by this module. But a strong passphrase should be of ample length, and should contain characters beyond the standard alphabet.

Checks the database to verify that the user exists. As this method is used internally frequently its positive result is cached to minimize database queries. Methods that would invalidate the existence of the user in the database, such as $user->delete_user will remove the cache entry, and subsequent tests will access the database on each call to exists_user(), until such time that the result flips to positive again.

A typical usage probably won't require calling this function directly very often, if at all. In most cases where it would be useful to look at the salt, the passphrase digest, and IP whitelists, the $user->validate( $passphrase, $ip ) method is easier to use and less prone to error. But for those cases I haven't considered, the get_credentials() method exists.

Returns a list containing the whitelisted IP's for this user. Each IP will be a string in the form of 192.168.0.198. If the user doesn't use IP validation, or there are no IP's stored for this user, the list will be empty.

Email addresses are not verified for validity in any way. However, the default database field used for storing email addresses provides 320 bytes of storage, which is the maximum length possible for a valid email address.

Using the same algorithms of add_user( { password = $passphrase } ); >>, creates a new password for the user. If the old passphrase is supplied as a second parameter, the update will only take place if the old passphrase validates.

The "with validation" method is useful for allowing a user to update her own password. The "without validation" version is useful for allowing an administrator (or automated process) to reset a user's forgotten password.

Returns Boolean true if and only if the user can be validated. What that means will be described in the paragraphs below. If the user cannot be validated, the return value will be Boolean false. It doesn't matter what the reason for failure to authenticate might have been: Invalid user ID, invalid password, or invalid IP address; all three reasons result in a return value of Boolean false.

This design decision encourages the best practice of not divulging to the user why his authentication failed. The less information provided, the less an attacker can user to narrow the field. If it is necessary to explicitly test whether a userid actually exists, or whether the user's IP matches the whitelist, separate accessors are provided to facilitate that requirement.

If you wish to force a revalidation (assume a dirty cache) you have two options:

If the user has been configured for no IP testing, validation means that the userid exists (case insensitively) within the database, and that the passphrase passed to validate(), when salted with the stored salt and digested using a SHA2-512 hashing algorithm results in the same 512 bit hash as the one generated when the passphrase was originally set up.

If the user has been configured to require IP testing, validation also means that the IP supplied to the validate() method matches one of the IP's stored in the database for this user. IP's are stored in the clear, which shouldn't matter. User input should never be used for the IP field of validate(). It is assumed that within the application, the user's IP will be detected, and that IP will be passed for cross-checking with the whitelist database.

The validate() method caches its positive result. Any action that might change the authentication status will remove the cached status. Actions that will result in validate() to perform all tests again include delete_user(), update_password(), or validated(0) (passing the validated() method a '0'.

Returns true if the user has been validated, as described above. Does not perform a full validation; simply tests whether the previous call to validate() succeeded, and that nothing has happened to remove that "is valid" status.

Pass a parameter of '0' to force all future calls to validated() to return false. Also, after resetting validated() to false, future calls to validate() will go through the full authentication process again until such time as the authentication is successful.

Please refer to the contents of the examples/ directory from this distribution's build directory. There you will find a working example that creates some roles, some privileges, assigns the privileges to a role, creates some domains, creates a user, and associates a role and several domains to that user. Though the example doesn't exercise every method contained in the distribution, it provides a nice concise demonstration of setting up the basic elements of Authentication and RBAC, and using them.

This module requires DBIx::Connector, Authen::Passphrase::SaltedSHA512, and List::MoreUtils. It also requires a database connection. The test suite will use DBD::SQLite, but it has also been tested with DBD::mysql. None of these dependencies with the exception of List::MoreUtils could be considered light-weight. The dependency chain of this module is indicative of the difficulty in assuring cryptographically strong random salt generation, reliable SHA2-512 hashing of passphrases, fork-safe database connectivity, and transactional commits for inserts and updates spanning multiple tables.

For convenience, a class method has been provided with each of this distribution's classes that will auto-generate the minimal schema within a SQLite or MySQL database. The SQLite database is probably only useful for testing, as it lacks many of the security measures present in web-stack quality databases.

Within the ./scripts/ directory of this distribution you will find a script that accepts a database type (mysql or sqlite), database name, database username, and database password on the command line. It then opens the given database and creates the appropriate tables. The script is named cudbi-configdb. Run it once without any command line parameters to see details on usage.

After creating the database framework, it might be useful to alter the tables that have been generated by customizing field widths, text encoding, and so on. It may be advisable to enable UTF8 for the userid, email, username fields, and possibly even for the role field.

There is no explicit size requirement for the userid, username, and role fields. They could be made wider if it's deemed useful. Don't be tempted to reduce the size of the email address field: The best practice of coding to the standard dictates that the field needs to be 320 characters wide.

The salt and password fields are used to store a 128 hex-digit representation of the 512 bit salt and 512 bit SHA2 hash of the user's passphrase. More digits is not useful, and less won't store the full salt and hash.

If you find that your particular database engine is not playing nicely with the test suite from this module, it may be necessary to provide the database login credentials for a test database using the same engine that your application will actually be using. You may do this by setting $ENV{CUDBI_TEST_DSN}, $ENV{CUDBI_TEST_DATABASE}, $ENV{CUDBI_TEST_USER}, and $ENV{CUDBI_TEST_PASS}.

Currently the test suite tests against a SQLite database since it's such a lightweight dependency for the testing. The author also uses this module with several MySQL databases. As you're configuring your database, providing its credentials to the tests and running the test scripts will offer really good diagnostics if some aspect of your database tables proves to be at odds with what this module needs.

Be advised that the the test suite drops its tables after completion, so be sure to run the test suite only against a database set up explicitly for testing purposes.

This module has only been tested on MySQL and SQLite database engines. If you are successful in using it with other engines, please send me an email detailing any additional configuration changes you had to make so that I can document the compatibility, and improve the documentation for the configuration process.