z, ? | toggle help (this) |
space, → | next slide |
shift-space, ← | previous slide |
d | toggle debug mode |
## <ret> | go to slide # |
r | reload slides |
n | toggle notes |
Hello everyone. I am excited and honored to be speaking here at RubyConf LT.|My presentation today is entitled Rodauth: Website Security Through Database Security, and I will discuss security issues related to website authentication, and how Rodauth is designed to handle these issues.
My name is Jeremy Evans, and I am the maintainer of numerous ruby libraries,
the most popular of which is Sequel, the database toolkit for Ruby.
For the last couple of years, I have also been working on Roda, which is a full featured toolkit for building maintainable and fast web applications in ruby.
As most of you know, Ruby’s most popular web framework is Rails. One of the disadvantages to using a ruby web framework that is not Rails, is that so many libraries are built to work just with Rails.
If you are looking for an authentication framework, and you are using Rails, there is Devise, Authlogic, and Sorcery.
In the past, I have worked mostly on non-Rails applications, and as there was not a good authentication library I could use, I ended up implementing authentication differently in each of the applications I worked on, based on the needs of the application.|Last year, after looking at existing solutions and not finding any that would work for all of my applications, I decided to build an authentication framework, which I named Rodauth.
As I was developing Rodauth, one of the main design goals was that it should be flexible, as I had many applications with different authentication requirements.|I had applications where password hashes were stored in the same table as the logins, applications that authenticated against LDAP, applications needing separate authentication for separate groups of users, and applications that used database functions for authentication.
Another design goal was the default settings should be as secure as possible, and specifically make it difficult for an attacker to get access to password hashes. I will explain later why this is so important.
The final design goal is that Rodauth should be simple to use and configure. One of the most common security issues in applications is misconfiguration, and making configuration as simple as possible leads to more secure applications.
I also wanted Rodauth to be full featured. I wanted to be able to start a new application and with a few lines of code enable not just login authentication, but account creation and verification, password changing and resets, and brute force attack protection.|Other than the Rails-dependent authentication frameworks, most other ruby authentication libraries only offer login authentication, leaving you to implement the other features yourself.|So that is the history behind Rodauth and its design goals. Let me now go over the sections in the rest of this presentation.
I am first going to talk about how Rodauth attempts to protect against password hash cracking, stopping attackers from using password hashes stored in your system to break into other systems.
I will explain how Rodauth protects from the reverse, where another system has been attacked and its password hashes have been cracked, and attackers are attempting to use the cracked passwords to break into accounts in your system.
I will talk about how Rodauth handles tokens used for password resets and account verification,
and briefly discuss each of the many features Rodauth offers.
I will talk about Rodauth’s configuration DSL and the flexibility it offers to handle most authentication requirements, as well as some interesting aspects of Rodauth’s internals.
Finally, I will show how Rodauth can be integrated into existing systems. It does not matter if you are using Rails, Sinatra, Roda, Hanami, or are running on bare Rack, you can use Rodauth in your application.
I am now going to explain what makes Rodauth different from other authentication libraries from a security perspective, which is the attempt to protect against password hash cracking by the use of multiple database accounts and database functions for authentication by default. This makes it difficult for attackers to even get access to the underlying password hashes.
First, why is access to password hashes so important. Well, it stems from the fact that most humans are bad at remembering good passwords. There are two common mistakes that many humans make in regards to passwords.
The first mistake is that humans tend to choose simple passwords. If you try to force them to use complex passwords, many humans will pick a simple password that meets your complexity requirements.
If you do not have any requirements on passwords, many humans will use passwords like 123456 or the word password.
If you require that passwords must be at least eight characters and contain uppercase letters, lowercase letters, and numbers, many humans will use passwords like Password1. This is not significantly more secure than just password by itself.
Password complexity requirements only make it more likely that humans will write down the password. If a human will not use a secure password in the absense of password complexity requirements, it is unlikely they will use a secure password if you have password complexity requirements.
The second mistake that humans make in regards to passwords is that humans tend to reuse passwords on multiple sites. It is impractical to attempt to prevent this.
These two mistakes, taken together, mean that even if you are not storing any important data in your own system, if you are storing password hashes for users, and your application gets compromised, attackers can try to crack the password hashes stored in your system, and use the passwords to access other systems.
In order for an attacker to use password hashes from one system to attack another system, the first thing the attacker needs to do is get access to the password hashes. Without access to the password hashes, there is nothing the attacker can use to attack other systems. So as much as possible, you should make it difficult for an attacker to access the password hashes.
Before I discuss how Rodauth protects access to the password hashes, let me first discuss the worst case scenario, where an attacker is able to access the password hashes. The first thing they will probably do is to attempt to crack the hashes.
This is generally done first using a dictionary attack.
A dictionary attack basically tries each word in a dictionary, in order. In English, there are only about a million words in the dictionary, so any password that is also a word that appears in a dictionary will be cracked quickly. There many variations on dictionary attacks.
aardvark
aardwolf
aback
...
One variation on a dictionary attack uses each dictionary word as a prefix, and checks different suffixes. Passwords that use simple additions like this will also be cracked quickly.
aardvark1
aardvark2
...
aardvark9
aardwolf1
aardwolf2
...
Another uses common substitutions of symbols and numbers for letters. Passwords that use substitutions like this also tend to be cracked quickly.
@ardvark
a@rdvark
aardv@rk
...
@ardwolf
a@rdwolf
aardw0lf
aardwo1f
...
If a modified dictionary attack does not crack the password hash, the next type of attack is usually a brute force attack.
A brute force attack basically tries all possible combinations of characters, and will eventually be able to crack any password given enough time.
aaaa
aaab
...
zzzy
zzzz
aaaaa
aaaab
...
zzzzz
aaaaaa
The amount of time it takes on average to crack a password hash via brute force depends on the length of the password and the character set it uses, as that decides the universe of possible passwords.
Here is a table showing how many combinations there are for passwords of a given length and character set.
[a-z]{4} | 456976
[a-z]{8} | 2 * 1011
[a-z0-9]{8} | 3 * 1012
[A-Za-z]{8} | 5 * 1013
[A-Za-z0-9]{8} | 2 * 1014
[a-z]{11} | 3 * 1015
There are fewer than a half million 4 character all lowercase passwords
[a-z]{4} | 456976
[a-z]{8} | 2 * 1011
[a-z0-9]{8} | 3 * 1012
[A-Za-z]{8} | 5 * 1013
[A-Za-z0-9]{8} | 2 * 1014
[a-z]{11} | 3 * 1015
but more than 200 billion possible 8 character all lowercase passwords
[a-z]{4} | 456976
[a-z]{8} | 2 * 1011
[a-z0-9]{8} | 3 * 1012
[A-Za-z]{8} | 5 * 1013
[A-Za-z0-9]{8} | 2 * 1014
[a-z]{11} | 3 * 1015
and more than 3 quadrillion 11 character all lowercase passwords.
[a-z]{4} | 456976
[a-z]{8} | 2 * 1011
[a-z0-9]{8} | 3 * 1012
[A-Za-z]{8} | 5 * 1013
[A-Za-z0-9]{8} | 2 * 1014
[a-z]{11} | 3 * 1015
One thing to note here is that less complex but longer passwords are in general more secure than more complex but shorter passwords. An 11 character all lower case password is about 16 times as secure as an 8 character password with uppercase letters, lowercase letters, and numbers.
[a-z]{4} | 456976
[a-z]{8} | 2 * 1011
[a-z0-9]{8} | 3 * 1012
[A-Za-z]{8} | 5 * 1013
[A-Za-z0-9]{8} | 2 * 1014
[a-z]{11} | 3 * 1015 16x
You can use that information to choose better passwords for your own accounts. As I mentioned earlier, you cannot force humans to use good passwords. However, you can choose which password hash algorithm to use, and that can have a dramatic effect on password hash cracking times.
Many of you are probably familar with the SHA1 and SHA256 hash algorithms. While these are decent cryptographic hash algorithms, they are bad password hash algorithms.|First, these are designed for speed, and you generally want password hashing to be slow, not fast, in order to make cracking the password hashes more difficult.|Second, these hashes do not include salts, which means that when using them for password hashing, the same password always results in the same hash.
If you take the SHA1 hash of the word password, it always results in the same hash.
$ echo -n password | sha1
5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
$ echo -n password | sha1
5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
That is bad from a security perspective, as it makes it possible to crack the password hashes using a precomputed rainbow table, allowing all simple passwords to be cracked immediately.|A rainbow table of all 9 character alphanumeric SHA1 password hashes takes only 690GB of storage, and would allow immediate cracking of any of these passwords hashes.
To prevent a rainbow table attack, you do what is called salting, where you add random data called a salt before the password, and when you check a submitted password, you prepend the salt for the current password hash to the submitted password, you compute the password hash, and then you check if the resulting hash matches the current password hash.
In addition to salting, the other way to increase password security is to require more computation to construct the hash. Good password hash algorithms use many iterations of an underlying algorithm internally, which is referred to as streching.
These days, there are two good password hash algorithms. One is bcrypt, another is scrypt. bcrypt is older and has been studied more, scrypt is newer and takes more memory in addition to a lot of CPU time, making it theoretically more resistant to some attacks using custom hardware. Rodauth currently uses bcrypt.
If you take the bcrypt hash of the word password, it results in different output, due to the inclusion of a random salt.
$ echo -n password | encrypt
$2b$08$tO1zyO2F8wRwISMvDg.YCuLUPoMDGwVPpl76vf5bXng3E4bRRCoui
$ echo -n password | encrypt
$2b$08$WdUcdTDMVgTNUFeQb/kWku7hfAf9R0JcHzwSb90NQPIpbF7tqiojO
The highlighted part here is the salt for the password, with the remaining part being the hash itself. So to check a bcrypt password for validity, it first prepends the salt to the provided password, then it runs the bcrypt algorithm over the result, and then checks if the resulting hash matches.
$ echo -n password | encrypt
$2b$08$tO1zyO2F8wRwISMvDg.YCuLUPoMDGwVPpl76vf5bXng3E4bRRCoui
$ echo -n password | encrypt
$2b$08$WdUcdTDMVgTNUFeQb/kWku7hfAf9R0JcHzwSb90NQPIpbF7tqiojO
Bcrypt includes a cost factor as part of the salt, which each increase in the cost factor doubling the amount of work bcrypt will perform to compute the hash. In this case, the cost factor is 8. I’ll use the term bcrypt(8) to refer to bcrypt with cost factor 8.
$ echo -n password | encrypt
$2b$08$tO1zyO2F8wRwISMvDg.YCuLUPoMDGwVPpl76vf5bXng3E4bRRCoui
One of my test machines, which is fairly old, can compute 7 million SHA1 password hashes per second. It can compute 80 bcrypt(8) password hashes per second. So bcrypt(8) is almost 100,000 times harder to crack than SHA1.
If we consider the time to check each password hash by the number of possible passwords, we can compare about how long it would take to crack the average password in each of these sets, depending on whether the hashes use SHA1 or bcrypt(8).
set | # | SHA1 | bcrypt(8)
[a-z]{4} | 456976 | <1 second | 1.5 hours
[a-z]{8} | 2 * 1011 | 8 hours | 82 years
[a-z0-9]{8} | 3 * 1012 | <2 days | 1 millenium
[A-Za-z]{8} | 5 * 1013 | 88 days | 21 millenia
[A-Za-z0-9]{8} | 2 * 1014 | <1 year | 86 millenia
[a-z]{11} | 3 * 1015 | 16 years | 1454 millenia
One thing we can see is that it takes more time to crack an 8 character password hashed with bcrypt(8) than it takes to crack an 11 character password hashed with SHA1.|Note that these numbers were for a older single machine. Anyone serious about cracking password hashes could easily spin up tens of thousands of more powerful cloud machines dedicated to cracking.|Let us see what the numbers look like for an serious attacker.
set | # | SHA1 | bcrypt(8)
[a-z]{4} | 456976 | <1 second | 1.5 hours
[a-z]{8} | 2 * 1011 | 8 hours | 82 years
[a-z0-9]{8} | 3 * 1012 | <2 days | 1 millenium
[A-Za-z]{8} | 5 * 1013 | 88 days | 21 millenia
[A-Za-z0-9]{8} | 2 * 1014 | <1 year | 86 millenia
[a-z]{11} | 3 * 1015 | 16 years | 1454 millenia
As you can see, against a serious attacker, all simple SHA1 password hashes can be cracked almost immediately, and most simple bcrypt passwords could be cracked if the attacker is willing to wait.|Note that the numbers for SHA1 here are not even considering the use of graphics processors. SHA1 hashes can be cracked up to 500 times faster on a graphics processor than on a CPU. However, graphics processors are slower than CPUs for cracking bcrypt hashes, due to bcrypt’s design, which requires constant memory access. This is another reason to use bcrypt over SHA1.
set | # | SHA1 | bcrypt(8)
[a-z]{4} | 456976 | <1 second | <1 second
[a-z]{8} | 2 * 1011 | <1 second | 43 minutes
[a-z0-9]{8} | 3 * 1012 | <1 second | 9 hours
[A-Za-z]{8} | 5 * 1013 | 7 seconds | 8 days
[A-Za-z0-9]{8} | 2 * 1014 | 30 seconds | 31 days
[a-z]{11} | 3 * 1015 | 9 minutes | 1.5 years
I should also point out that these numbers are for bcrypt(8), where Rodauth’s default is bcrypt(10), which is 4 times harder to crack.
set | # | SHA1 | bcrypt(8)
[a-z]{4} | 456976 | <1 second | <1 second
[a-z]{8} | 2 * 1011 | <1 second | 43 minutes
[a-z0-9]{8} | 3 * 1012 | <1 second | 9 hours
[A-Za-z]{8} | 5 * 1013 | 7 seconds | 8 days
[A-Za-z0-9]{8} | 2 * 1014 | 30 seconds | 31 days
[a-z]{11} | 3 * 1015 | 9 minutes | 1.5 years
Now we can answer the question of why protecting access to password hashes is so important. It is because if attackers get access to the password hashes, they can crack the hashes to get the passwords, and then they can use the passwords to attack other systems. So protecting access to password hashes is critical from a security perspective.
How do you prevent an attacker from accessing the password hashes, while still checking passwords for validity during login?
Rodauth’s approach is store password hashes in a separate table.
Rodauth by default uses the accounts table to store accounts
CREATE TABLE accounts (
id integer PRIMARY KEY,
email text NOT NULL
);
and the account_password_hashes table to store password hashes.
CREATE TABLE accounts (
id integer PRIMARY KEY,
email text NOT NULL
);
CREATE TABLE account_password_hashes (
id integer PRIMARY KEY REFERENCES accounts,
password_hash text NOT NULL
);
The account_password_hashes table has a foreign key reference to the accounts table that is also the primary key, ensuring there is only a single password hash for each account.
CREATE TABLE accounts (
id integer PRIMARY KEY,
email text NOT NULL
);
CREATE TABLE account_password_hashes (
id integer PRIMARY KEY REFERENCES accounts,
password_hash text NOT NULL
);
Rodauth uses two separate database accounts.
One database account is used by the application itself, and has access to most of the tables, including the accounts table. For purposes of this presentation, I will call this account app.
-- Owned by application database account (app)
CREATE TABLE accounts (
id integer PRIMARY KEY,
email text NOT NULL
);
CREATE TABLE account_password_hashes (
id integer PRIMARY KEY REFERENCES accounts,
password_hash text NOT NULL
);
A separate database account owns the password hash table. I will refer to this account as ph.
-- Owned by application database account (app)
CREATE TABLE accounts (
id integer PRIMARY KEY,
email text NOT NULL
);
-- Owned by password hash database account (ph)
CREATE TABLE account_password_hashes (
id integer PRIMARY KEY REFERENCES accounts,
password_hash text NOT NULL
);
After the ph account creates the password hash table, it revokes access to the password hash table from all users.
-- Owned by application database account (app)
CREATE TABLE accounts (
id integer PRIMARY KEY,
email text NOT NULL
);
-- Owned by password hash database account (ph)
CREATE TABLE account_password_hashes (
id integer PRIMARY KEY REFERENCES accounts,
password_hash text NOT NULL
);
REVOKE ALL ON account_password_hashes
FROM public;
Then the ph account grants access to insert, update, and delete password hashes to the app account. This allows the app account to insert password hashes for new users, remove password hashes for deleted users, and change password hashes for existing users. However, it does not allow the app account access to read the password hash table.
-- Owned by application database account (app)
CREATE TABLE accounts (
id integer PRIMARY KEY,
email text NOT NULL
);
-- Owned by password hash database account (ph)
CREATE TABLE account_password_hashes (
id integer PRIMARY KEY REFERENCES accounts,
password_hash text NOT NULL
);
REVOKE ALL ON account_password_hashes
FROM public;
GRANT INSERT, UPDATE, DELETE ON
account_password_hashes TO app;
In order to authenticate without allowing access to the hashes, Rodauth uses two database functions, both of which are created by the ph account.
The first function is called rodauth_get_salt, which returns the password salt.
CREATE OR REPLACE FUNCTION
rodauth_get_salt(account_id integer)
RETURNS text AS $$
DECLARE salt text;
BEGIN
SELECT substr(password_hash, 0, 30) INTO salt
FROM account_password_hashes
WHERE account_id = id;
RETURN salt;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
This function takes the account id as an argument.
CREATE OR REPLACE FUNCTION
rodauth_get_salt(account_id integer)
RETURNS text AS $$
DECLARE salt text;
BEGIN
SELECT substr(password_hash, 0, 30) INTO salt
FROM account_password_hashes
WHERE account_id = id;
RETURN salt;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
It finds the row with the matching id.
CREATE OR REPLACE FUNCTION
rodauth_get_salt(account_id integer)
RETURNS text AS $$
DECLARE salt text;
BEGIN
SELECT substr(password_hash, 0, 30) INTO salt
FROM account_password_hashes
WHERE account_id = id;
RETURN salt;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
Now, the account_password_hashes table stores the bcrypt password hash in a single column. So in order to get the salt, it needs to extract the salt portion of the hash, using the substring function.
CREATE OR REPLACE FUNCTION
rodauth_get_salt(account_id integer)
RETURNS text AS $$
DECLARE salt text;
BEGIN
SELECT substr(password_hash, 0, 30) INTO salt
FROM account_password_hashes
WHERE account_id = id;
RETURN salt;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
The important part is the use of SECURITY DEFINER when creating the function. Setting SECURITY DEFINER for a database function is similar to setting an executable as setuid in Unix, in that the function executes using the permissions of the user that defined the function, instead of the user that is executing the function.|This is what allows the app account to check passwords for validity. While the app account cannot read the password hashes, it can call this function, which runs as the ph account and can read the password hashes.
CREATE OR REPLACE FUNCTION
rodauth_get_salt(account_id integer)
RETURNS text AS $$
DECLARE salt text;
BEGIN
SELECT substr(password_hash, 0, 30) INTO salt
FROM account_password_hashes
WHERE account_id = id;
RETURN salt;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
After the application retrieves the salt for the current password hash, it uses the password provided by the user to compute the hash. Then it needs to call a database function to check if the resulting hash is valid.
That function is rodauth_valid_password_hash.
CREATE OR REPLACE FUNCTION
rodauth_valid_password_hash
(account_id integer, hash text)
RETURNS boolean AS $$
DECLARE valid boolean;
BEGIN
SELECT password_hash = hash INTO valid
FROM account_password_hashes
WHERE account_id = id;
RETURN valid;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
This function takes the account id and the password hash computed by the application.
CREATE OR REPLACE FUNCTION
rodauth_valid_password_hash
(account_id integer, hash text)
RETURNS boolean AS $$
DECLARE valid boolean;
BEGIN
SELECT password_hash = hash INTO valid
FROM account_password_hashes
WHERE account_id = id;
RETURN valid;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
It finds the row with the matching id.
CREATE OR REPLACE FUNCTION
rodauth_valid_password_hash
(account_id integer, hash text)
RETURNS boolean AS $$
DECLARE valid boolean;
BEGIN
SELECT password_hash = hash INTO valid
FROM account_password_hashes
WHERE account_id = id;
RETURN valid;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
And it returns whether the password hash computed by the application matches the stored password hash. If so, the user provided the correct password and should be allowed to login. Otherwise, the user provided an incorrect password and should not be allowed to login.
CREATE OR REPLACE FUNCTION
rodauth_valid_password_hash
(account_id integer, hash text)
RETURNS boolean AS $$
DECLARE valid boolean;
BEGIN
SELECT password_hash = hash INTO valid
FROM account_password_hashes
WHERE account_id = id;
RETURN valid;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
This approach of disallowing the application access to read password hashes offers protections against two types of attacks.
The most common way that attackers get access to password hashes is by exploiting SQL injection vulnerabilities. With Rodauth’s approach, an attacker cannot get access to the password hash table via an SQL injection vulnerability.
What happens if an attacker tries to read all password hashes directly? This simple SELECT query to read the password hash table
SELECT * FROM account_password_hashes;
results in an error, since the app account does not have access to read this table.
SELECT * FROM account_password_hashes;
ERROR: permission denied for
relation account_password_hashes
More sophisticated attackers might try to grant themselves access to the table first.
GRANT SELECT ON account_password_hashes
TO my_account;
While this does not result in an error, it also does not make any changes. The reason for this is the app account is not the owner of the password hash table, and only the owner of the table or a superuser can grant privileges on the table.
GRANT SELECT ON account_password_hashes
TO my_account;
WARNING: no privileges were granted
for "account_password_hashes"
Maybe an attacker will try to change the owner of the table?
ALTER TABLE account_password_hashes
OWNER TO my_account;
This also raises an error, as you cannot change the owner of a table unless you are the owner of the table.
ALTER TABLE account_password_hashes
OWNER TO my_account;
ERROR: must be owner of relation
account_password_hashes
Basically, Rodauth’s approach is secure against SQL Injection.
What about a remote code execution vulnerability? This is the worst type of vulnerability in an application, as the attacker can run arbitrary code.|The good news is Rodauth is secure against this type of attack on stored password hashes. Unless the attacker can get access to either the ph account or a database superuser account, they cannot get access to the password hashes.
However, if an attacker can remotely execute code, while they may not be able to get access to stored password hashes, they can probably compromise any account that logs in while they have control of the system. They do not need the stored password hash in this case, as a user logging in provides the plain text of their password.
If an attacker can exploit a remote code execution vulnerability in your application, they may be able to escalate their privileges to get access to a database superuser account, database files, or database backups. To mitigate against that risk you need to isolate your database from your application, which is a bit out of scope for this talk.
While an attacker that could exploit a remote code execution vulnerability would not have direct access to stored password hashes, there is a possible timing attack which may allow them to more easily guess password hashes.
If we go back to the database function used for checking password hashes, it just does a simple comparison of the existing hash to the provided hash. This means that the time this function will take to return a result is in some way dependent on how many initial characters of the provided password hash match the stored password hash.|It is theoretically possible for attacker to exploit this to determine the stored password hash more quickly, though I expect such an attack would be difficult. This type of attack could be mitigated by using a timing safe string comparison function, but unfortunately most databases do not provide one.
CREATE OR REPLACE FUNCTION
rodauth_valid_password_hash
(account_id integer, hash text)
RETURNS boolean AS $$
DECLARE valid boolean;
BEGIN
SELECT password_hash = hash INTO valid
FROM account_password_hashes
WHERE account_id = id;
RETURN valid;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
There are some limitations with Rodauth’s approach of using multiple database accounts and database functions for authentication. First, it requires that you have the ability to use multiple accounts in your database. This is fairly easy if you control the database,
but if you are using a service like Heroku to supply the database, you may not be able to use multiple database accounts.
Second, Rodauth only has built in support for setting up the necessary database authentication functions on PostgreSQL, MySQL, and Microsoft SQL Server . The approach Rodauth uses should be portable to other databases that support multiple accounts and functions that execute with the privileges of the user that defined the function.
When you cannot use multiple database accounts, or when using a database that does not support the database authentication functions, Rodauth can access the password hash table directly, which drops the security level to roughly the same as other Ruby authentication frameworks.|Rodauth also supports storing the password hash in the same table as the accounts, allowing it to work with existing databases using that schema.
In review, Rodauth protects access to password hashes using multiple database accounts and database functions, and a secure password hash algorithm to slow attackers down if they do get access to the password hashes.
Protecting access to password hashes prevents attackers from using password hashes from your system to attack other systems. How do you protect against the reverse, with attackers using passwords hashes from other systems to attack your system?
A good way to prevent that is to require something in addition to the password when authenticating the user. This is referred to as 2FA
or 2 factor authentication. With 2 factor authentication, the first factor is the password, and users must also provide a second factor to authenticate. This second factor can be a physical object, biometric, rotating pass code, or something else.
One way to integrate 2 factor authentication into your application is to have users register their mobile phone number, and send SMS messages to that phone number containing codes that the user must enter.
This does require all of your users have a working mobile phone on which to receive SMS codes in order to login, which can be problematic.
One open standard for 2 factor authentication is U2F
or Universal 2nd Factor. With U2F, the user must have a separate physical USB security key, as well as a compatible browser. Logging in with U2F requires inserting the security key, and pressing a button on the security key after typing in the user name and password.
The main problem with U2F is it requires a separate security key. Unlike mobile phones, users are not likely to already have a U2F-compatible security key, and are not as likely to keep it with them as they are a mobile phone.
Additionally, only Google Chrome currently has native support for U2F, so users using other browsers cannot use U2F without a separate plugin or extension.
Another open standard for 2 factor authentication is TOTP
which stands for Time-Based One-Time Password.
TOTP is described by RFC 6238, which was published in 2011, so it has been around for a while.
TOTP is time based. That means that as time passes, the authentication code to login changes. Many TOTP implementations default to rotating the authentication code every 30 seconds.
The current authentication code may be 52 64 61,
but in 30 seconds the authentication code may change to 75 74 68
TOTP works by having the client and server share a secret, which is usally displayed as a 16 character string.
If you want to store the TOTP secret on your mobile phone, you can scan in a QR code containing the secret using the phone’s camera.
The are many mobile apps that support TOTP, scanning in secrets via QR codes, and displaying the current authentication code for the system. Google Authenticator is probably the most popular app that supports TOTP.
However, a mobile phone is not required, there are plenty of other tools that handle TOTP. There are many libraries, command line programs, and graphical programs that support TOTP.
TOTP just requires an additional form element when logging in, and works in all browsers.
Because TOTP relies on time and codes are changed often, it is dependent on the client’s clock being synchronized with the server’s clock. This is not a problem in general as most computers and mobile phones synchronize to the same root time source via network time protocol.
Because of the wider support for TOTP as well as ease of use and implementation, I choose TOTP as the basis for Rodauth’s 2 factor authentication support.
Rodauth’s TOTP feature is separate from the login feature. This makes it easy to support a wide variety of authentication needs. You can force all users that have accounts to use TOTP. You can make TOTP use optional and only require users use it if they sign up for it. You can also only require TOTP use for sensitive actions.
One of the issues 2 factor authentication implementations have to deal with is how to handle the situation where the usual second factor is not available, for example with a lost mobile phone.
Rodauth supports the use of recovery codes, which are single use passwords that can be used instead of TOTP authentication codes.
Rodauth also supports sending authentication codes via SMS, if users have registered their mobile phone number.
Another issue 2 factor authentication implementations have to deal with is how to handle brute force attacks. TOTP authentication codes and SMS authentication codes are usually 6 decimal digits, with means there are only 1 million combinations, which is fairly trivial to brute force.
Rodauth defaults to locking out TOTP authentication and SMS authentication after 5 failed attempts. After TOTP authentication and SMS authentication have been locked out, a recovery code has to be entered in order to gain access to the account.
Rodauth defaults to requiring a user to reenter their password whenever changing their TOTP security settings, as well as already being authenticated via TOTP before disabling 2 factor authentication or viewing recovery codes.
By using Rodauth’s 2 factor authentication support, you can protect your system from password hash attacks on other systems, in addition to protecting other systems from password hash attacks on your system.
I will now briefly discuss Rodauth’s approach to token security.
First, what types of tokens are we talking about? Well, if a user forgets their password and needs to reset it, Rodauth generates a password reset token for the user and emails it to them.|The user receives the email, which contains a link with the token, allowing them to reset their password. Similar tokens are used for other features in Rodauth.
One of the security advantages to Rodauth’s tokens is that each token is account-specific. In Rodauth, a password reset token for one user would never be valid for another user. Other authentication libraries I reviewed appear to use purely random tokens.
Rodauth tokens use the following format.
1234_hJxZkHnb9O5XoA1916It_apCSQVJmB6cCgxbxHbKiOU
The first part of the token is the users account id, which is the part of the token that is account specific.
1234_hJxZkHnb9O5XoA1916It_apCSQVJmB6cCgxbxHbKiOU
The remainder of the token is randomly generated.
1234_hJxZkHnb9O5XoA1916It_apCSQVJmB6cCgxbxHbKiOU
To authenticate the token, it is first split into two parts, the account id and the key.
{
account_id: 1234,
key: 'hJxZkHnb9O5XoA1916It_apCSQVJmB6cCgxbxHbKiOU'
}
Rodauth will look in the table storing the tokens, and find the row matching the account.
{
account_id: 1234,
key: 'hJxZkHnb9O5XoA1916It_apCSQVJmB6cCgxbxHbKiOU'
}
Rodauth then checks if the key given matches the stored key. If so, the token is valid, otherwise, it is not.
{
account_id: 1234,
key: 'hJxZkHnb9O5XoA1916It_apCSQVJmB6cCgxbxHbKiOU'
}
The query Rodauth uses when retrieving a token is specific to the related account, returning the stored key.
{
account_id: 1234,
key: 'hJxZkHnb9O5XoA1916It_apCSQVJmB6cCgxbxHbKiOU'
}
SELECT key FROM account_password_reset_keys
WHERE id = 1234;
Using account specific tokens decreases the probability that a brute force attack on tokens will work. The issue with purely random tokens is that you can brute force attack all tokens at the same time.
Other authentication frameworks will lookup accounts via tokens using a query like this. The problem with this approach is that it increases the odds of a successful brute force attack into an arbitrary account by the number of rows that have a token.
SELECT id FROM accounts
WHERE password_reset_key =
'hJxZkHnb9O5XoA1916It_apCSQVJmB6cCgxbxHbKiOU';
This is mostly a theoretical vulnerability as long as the number of possible tokens is much larger than the number of accounts that have tokens, which should be true.
Another possible attack is a timing attack.
Because other authentication frameworks lookup by the key value, and have a database index on the key value, the time it takes to lookup up an account via a token depends on the number of characters at the start of the submitted token that match any existing token.
SELECT id FROM accounts
WHERE password_reset_key =
'hJxZkHnb9O5XoA1916It_apCSQVJmB6cCgxbxHbKiOU';
If a user submits this token, but there is an existing token in the system that starts with hJ but not hJx, it will in general take less time
SELECT id FROM accounts
WHERE password_reset_key =
'hJxZkHnb9O5XoA1916It_apCSQVJmB6cCgxbxHbKiOU';
than if there is an existing token that matches most of the start of the submitted token.
SELECT id FROM accounts
WHERE password_reset_key =
'hJxZkHnb9O5XoA1916It_apCSQVJmB6cCgxbxHbKiOU';
Exploiting timing attacks over the internet is fairly difficult, especially in this case as the difference in speed is very low and subject to subtantial variance. Because of those facts, this is also mostly a theoretical vulnerability.
Another difference between Rodauth and other authentication libraries is Rodauth uses separate tables to store each separate type of token, instead of storing all token data as separate columns in the accounts table.
If we go back to the Rodauth query I showed earlier, note that the table name it selects from is specific to password resets.
SELECT key FROM account_password_reset_keys
WHERE id = 1234;
This is not for security, it is for performance and storage space. If you have 10 million accounts, and only 1 thousand have password reset tokens at any one time, it is better for performance and storage space to have only 1000 rows of tokens, compared to having mostly NULL values for 10 million rows.
In review, Rodauth uses account specific tokens stored in separate tables, mostly for performance and storage space. It also tries to prevent theoretical brute force and timing attacks on tokens.
Next I am going to briefly describe each of Rodauth’s features. Rodauth attempts to support most common authentication and account management features.
Rodauth does not load any features by default. You need to explicitly load each feature you want to use. If you are not using a feature, you do not have to pay any memory or performance cost for that feature.
The most commonly used features are login and logout. Most of the discussion of password hash security related to how to authenticate users during login.
There is a feature that allows users to change their password,
as well as one that allows users to change their login.
If you have users that forget their passwords, you can enable a feature to allow them to automatically reset their passwords.
You can enable a feature that allows users to create their own accounts,
as well as a feature that requires users verify that they have access to the email address associated with the account.
You can also require that users reverify they have access to the email address associated with the account if they change the address.
There is also a feature for users to close their own accounts.
There is a feature called remember for automatically authenticating users based on a token stored in a cookie, and keeping track of whether they logged in via password or cookie,
which integrates with another feature that Rodauth offers for asking users to confirm their password for security sensitive actions.
To prevent bruteforce attacks, there is an account lockout feature that locks accounts out after a certain number of failed logins, as well as allowing users to unlock their account via email.
When verifying accounts, Rodauth by default does not allow the accounts to login before they have been verified, but you can allow a grace period for them to login before verifying.
Rodauth by default requires users to reinput their password for account modifications, but it offers a feature so that it does not ask for a password if one has been entered recently.
If you have a policy that requires password complexity checks, Rodauth has a feature that offers a configurable password complexity checker.
If you have a policy that disallows the reuse of recent passwords, Rodauth supports that.
If you have a policy that requires that users change their passwords on a regular basis, Rodauth supports that too.
Rodauth also supports automatically expiring unused accounts,
expiring sessions based on inactivity timeouts and max lifetimes,
as well as limiting each account to a single logged in session, automatically terminating any existing session when the user logs in.
Rodauth has an OTP feature implementing two factor authentication via time-based one time passwords.
Rodauth has an SMS codes feature implementing two factor authentication via codes sent in SMS messages.
Rodauth has a recovery codes feature implementing two factor authentication via single-use account recovery codes.
Finally, Rodauth has a JWT feature, which adds JSON API support to all of Rodauth’s features, storing authentication information in JWT tokens instead of rack sessions.
In review, Rodauth is a full featured authentication and account management framework, which should be able to handle the needs of most applications.
Rodauth is configured using a block based DSL that allows overriding all parts of the framework. This configuration approach is what makes Rodauth flexible enough to support most existing authentication needs.
Rodauth is implemented as a plugin to the Roda web toolkit. So this is the minimal usage of Rodauth in applications.|If you are using Roda, you can use Rodauth directly in your application as a plugin. If you are not using Roda, I will discuss how to use Rodauth in the next section.
require 'roda'
class App < Roda
plugin :rodauth do
enable :login, :logout
end
route do |r|
r.rodauth
end
end
All configuration for Rodauth happens inside the block you pass when loading the plugin.
require 'roda'
class App < Roda
plugin :rodauth do
enable :login, :logout
end
route do |r|
r.rodauth
end
end
The route block here is executed for each request received by the application.
require 'roda'
class App < Roda
plugin :rodauth do
enable :login, :logout
end
route do |r|
r.rodauth
end
end
In this example, all the route block does is call r.rodauth, which allows Rodauth to handle the request.
require 'roda'
class App < Roda
plugin :rodauth do
enable :login, :logout
end
route do |r|
r.rodauth
end
end
Since this section is focused on configuration, let me focus just on the configuration block
plugin :rodauth do
enable :login, :logout
end
I mentioned earlier that Rodauth does not load any features by default. In Rodauth, all features you want to use must be specified explicitly. You do this by calling enable with symbols for the features you want to use. In this case, they are the login and logout features.
plugin :rodauth do
enable :login, :logout
end
Each feature you load adds configuration methods related to the feature. So after loading the login feature, you can call the after_login configuration method with a block to specify additional behavior to perform after a successful login.|In this case, we are recording successful logins to a log.
plugin :rodauth do
enable :login, :logout
after_login do
LOGGER.info "#{account[:email]} logged" \
"in from #{request.ip}!"
end
end
The interesting parts here are the calls to account and request.|You see, the blocks you pass to each of these configuration methods are evaluated in the context of a Rodauth object. This object has access to everything related to the request.|So you can change how Rodauth will handle requests using any information related the request. This design is what makes Rodauth flexible enough to handle most authentication needs.
plugin :rodauth do
enable :login, :logout
after_login do
LOGGER.info "#{account[:email]} logged" \
"in from #{request.ip}!"
end
end
For simplicity, Rodauth allows you to use arguments for many simple configuration settings. The accounts_table method sets the database table used to store accounts. You can specify this by just passing a symbol to the method. This is a useful shortcut.
plugin :rodauth do
enable :login, :logout
accounts_table :users
end
However, you can pass a block to accounts_table if you want to be able to change which accounts table per request. All of Rodauth’s configuration methods accept blocks to allow more detailed control during configuration.
plugin :rodauth do
enable :login, :logout
accounts_table do
if request.ip.start_with?('192.168.1')
:admins
else
:users
end
end
end
I will describe briefly how this is accomplished internally, though I will simplify it a bit. The context in which the blocks are evaluated is an instance of a class I will call Auth.
class Auth
end
Each of the features you load into Rodauth is a separate module. The defaults for Rodauth are normal instance methods in these modules.
module Login
def after_login
end
end
When you enable the login and logout features in Rodauth, it is equivalent to including those modules into the Auth class.
class Auth
include Login
include Logout
end
And when you call one of the configuration methods, you are actually defining an instance method in the Auth class.
class Auth
include Login
include Logout
def accounts_table
if request.ip.start_with?('192.168.1')
:admins
else
:users
end
end
end
The configuration DSL is a shortcut to create an authentication class that includes correct modules and has certain methods overridden as needed.
plugin :rodauth do
enable :login, :logout
accounts_table do
if request.ip.start_with?('192.168.1')
:admins
else
:users
end
end
end
One consequence of this approach is that in all blocks given to configuration methods, you can call super to get Rodauth’s default behavior. Because you are defining methods via a block, you must pass explicit arguments to super, which in this case is no arguments.
plugin :rodauth do
enable :login, :logout
accounts_table do
if request.ip.start_with?('192.168.1')
:admins
else
super()
end
end
end
Another interesting aspect of Rodauth’s internals is while Login and Logout and modules, that is only part of the story.
class Auth
include Login
include Logout
end
They are not modules in the sense that they are not defined with the module keyword.
module Login
end
Rodauth actually has a class called Feature, which all of the features are instances of.
class Feature < Module
end
As you can see here, you can subclass Module in ruby in order to create custom module subclasses.
class Feature < Module
end
The reason for doing this is that you can then define instance methods in this module subclass. Here we define a depends method to set dependencies for the feature.
class Feature < Module
def depends(*deps)
dependencies.concat(deps)
end
end
Those module subclass instance methods are then callable as module methods when creating the feature instance. Here we call the depends method, to set that the lockout feature depends on the login feature. This makes it so loading the lockout feature will automatically load the login feature first.
Lockout = Feature.new do
depends :login
end
In addition to handling dependencies, Rodauth also needs to handle cases where two features interact without depending on each other. One example of this is the close account feature.
CloseAccount = Feature.new do
def after_close_account
DB[:account_password_hashes].
where(:account_id=>account_id_value).delete
end
end
After closing an account, the close account feature removes the account’s password hash.
CloseAccount = Feature.new do
def after_close_account
DB[:account_password_hashes].
where(:account_id=>account_id_value).delete
end
end
However, if you are using the reset password feature, you may also want any password reset keys to be deleted when an account is closed.|How do you handle the case where multiple features need to interact correctly when used together and also when used separately, and where the features can be added in either order?
CloseAccount = Feature.new do
def after_close_account
DB[:account_password_hashes].
where(:account_id=>account_id_value).delete
end
end
ResetPassword = Feature.new do
def after_close_account
DB[:account_password_reset_keys].
where(:account_id=>account_id_value).delete
end
end
I have found that the best way to accomplish this is to use super if defined?(super). This will call a superclass method if a superclass method exists, but do nothing if a superclass method does not exist. By doing this, you can have multiple features work properly when used together and when used separately, without either depending on the other.
CloseAccount = Feature.new do
def after_close_account
DB[:account_password_hashes].
where(:account_id=>account_id_value).delete
super if defined?(super)
end
end
ResetPassword = Feature.new do
def after_close_account
super if defined?(super)
DB[:account_password_reset_keys].
where(:account_id=>account_id_value).delete
end
end
In review, Rodauth is configured via a simple but flexible DSL, which allows it to handle the authentication and account management needs for most applications.
I would now like to show how you can easily integrate Rodauth into your applications.
First, I want to go over how Rodauth can be used in web applications that do not use the Roda web toolkit.
Roda supports a middleware plugin, which allows the Roda application to be used as Rack middleware. So to use Rodauth in an application that does not use Roda, you just create a small Roda app, load the middleware and rodauth plugins, and have the route block call r.rodauth.
require 'roda'
class RodauthApp < Roda
plugin :middleware
plugin :rodauth do
enable :login, :logout
end
route do |r|
r.rodauth
rodauth.require_authentication
env['rodauth'] = rodauth
end
end
use RodauthApp
In this case, if Rodauth does not handle the request and the user is not logged in, the Roda middleware will redirect them to the login page, which Rodauth will handle. If the user is logged in, any route that Rodauth does not handle will be passed to the application.
require 'roda'
class RodauthApp < Roda
plugin :middleware
plugin :rodauth do
enable :login, :logout
end
route do |r|
r.rodauth
rodauth.require_authentication
env['rodauth'] = rodauth
end
end
use RodauthApp
You can set the rodauth object in the request’s environment, making it easy for the application to get access to it.|You may be concerned about the extra overhead of using Rodauth via Roda middleware, but fear not, Roda is very light toolkit, and only adds about 2 megabytes to your application’s memory overhead.
require 'roda'
class RodauthApp < Roda
plugin :middleware
plugin :rodauth do
enable :login, :logout
end
route do |r|
r.rodauth
rodauth.require_authentication
env['rodauth'] = rodauth
end
end
use RodauthApp
For database access, Rodauth uses Sequel internally, but you can certainly use ActiveRecord in your application and still use Rodauth. You would have to setup a Sequel database connection, but that is pretty much it.|Sequel is not as small as Roda, but it still adds less than 8 megabytes to your application’s memory overhead.
require 'sequel'
DB = Sequel.connect('postgres://...')
plugin :rodauth do
enable :login, :logout
db DB
accounts_table :users
end
What if you want to authenticate using LDAP, say to Windows Active Directory? That can be accomplished using a single configuration method. You just call the password_match? configuration method with a block. Using the already retrieved account and the submitted password, you can use any custom authentication method that you want.
plugin :rodauth do
enable :login, :logout
password_match? do |password|
LdapAuth.valid?(account.username, password)
end
end
What if you want to have different authentication behavior for different sections of your application? Maybe you want to separate admin account authentication from regular account authentication.
plugin :rodauth do
enable :login, :logout
end
plugin :rodauth, :name=>:admin do
enable :login, :logout, :change_password
accounts_table :admins
password_hash_table :admin_password_hashes
end
You can support this by loading the Rodauth plugin multiple times with different names to store different Rodauth configurations.
plugin :rodauth do
enable :login, :logout
end
plugin :rodauth, :name=>:admin do
enable :login, :logout, :change_password
accounts_table :admins
password_hash_table :admin_password_hashes
end
Then, in your routing tree block, you can call r.rodauth with no arguments to use the default configuration.
route do |r|
r.on "admin" do
r.rodauth(:admin)
end
r.rodauth
end
But in the admin section of your site, you can call r.rodauth with the symbol admin, which will use the admin configuration.
route do |r|
r.on "admin" do
r.rodauth(:admin)
end
r.rodauth
end
Let us take a brief look at a real world example of Rodauth usage. This is from one of my open source applications called Giftsmas, which is a gift tracking program.
plugin :rodauth do
enable :login, :logout
session_key :user_id
login_param 'user'
login_label 'User'
login_column :name
accounts_table :users
account_password_hash_column :password_hash
title_instance_variable :@title
end
As I showed earlier, the first step is to enable the the required features, in this case login and logout.
plugin :rodauth do
enable :login, :logout
session_key :user_id
login_param 'user'
login_label 'User'
login_column :name
accounts_table :users
account_password_hash_column :password_hash
title_instance_variable :@title
end
These methods all override the default values so that the user interface for Giftsmas did not change when its custom authentication implementation was converted to use Rodauth.|No changes were required to the Giftsmas integration tests when converting it to use Rodauth.
plugin :rodauth do
enable :login, :logout
session_key :user_id
login_param 'user'
login_label 'User'
login_column :name
accounts_table :users
account_password_hash_column :password_hash
title_instance_variable :@title
end
Before I converted Giftsmas to use Rodauth, it already stored bcrypt password hashes in a column in the accounts table, so Giftsmas uses the account_password_hash_column configuration method.|I run the Giftsmas demo site on Heroku, so I cannot easily switch it to use Rodauth’s more secure default approach.
plugin :rodauth do
enable :login, :logout
session_key :user_id
login_param 'user'
login_label 'User'
login_column :name
accounts_table :users
account_password_hash_column :password_hash
title_instance_variable :@title
end
Hopefully these examples show how Rodauth can be integrated into existing applications, due to how flexible its configuration is.
Security, Simplicitity, and Flexibility. These are Rodauth’s three design goals.
If you value these goals and want to learn more about Rodauth, check out rodauth.jeremyevans.net for details.
I would like to end this presentation by discussing some ideas I would like you to take with you, not specifically related to Rodauth.
First, prefer longer passwords over shorter but more complex passwords. As I showed earlier, longer passwords can be significantly more secure than shorter but more complex passwords, and are often easier to remember.
Second, if you store password hashes, always use bcrypt or scrypt as the password hash algorithm. For important passwords, such as administrative passwords, consider using a higher than default cost factor.
Third, I would like you to consider using multiple database accounts in your applications where it makes sense from a security perspective. I rarely see applications that do this. While it does take some additional work to setup and maintain, if security is important, the benefits of doing so should outweigh the costs.
Fourth, recognize that database tables are cheap. In reviewing other authentication frameworks and other applications, I see a tendency to use tables with many NULLable columns, which is considered a smell from a database design perspective. Database tables are not a limited resource, do not be afraid to use more of them.
Finally, before creating your own library, review existing libraries that do the same or similar things. Before I started work on Rodauth, I reviewed Devise, Authlogic, and Sorcery.|Originally, my goal was to see if any of them would be flexible enough to handle the needs of all of my applications, and if it would be possible to use them without Rails.|Ultimately, I ended up writing Rodauth, but the knowledge I gained from reviewing existing libraries was very helpful to me in doing so.
That concludes my presentation. I would like to thank the organizers for inviting me to speak to all of you today, as well as thank all of you for listening to me.
If any of you have questions, I will be happy to answer them now.