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 |
Good afternoon everyone. Today I’ll be talking about Rodauth, Ruby’s most advanced authentication framework.
My name is Jeremy Evans. I’m the author of Rodauth. I’m also a Ruby committer, an OpenBSD committer, and the maintainer of the Sequel database library and the Roda web toolkit, as well as many other gems. When I’m not busy working on open source projects, I manage the IT operations for a small government department, including leading the programming team. I say team, but it’s really just me and one other programmer.
First, let me go over the agenda for this presentation. I’ll talk about the history of Rodauth, highlight some differences between Rodauth and other Ruby authentication frameworks, discuss Rodauth’s goals, and get into some code examples so you can get a good idea of how Rodauth is configured and how to use it.
First, a brief history of Rodauth. Rodauth was created in 2015. At that time I had over 10 separate applications I maintained that all had ad-hoc approaches to authentication. Authentication requirements differed significantly between the applications. Multiple applications used LDAP for authentication, while others stored password hashes in the database. All of the Ruby authentication frameworks at the time were specific to Rails, and none of my applications were Rails applications. So I got a crazy idea to write my own authentication framework, and it had to be flexible enough to handle the very different requirements of each application. After a review of other authentication frameworks, I also wanted this framework to take a different and stronger approach to password hash storage security, since this more secure approach was already in use in a few of my applications.
Now, about Rodauth’s tag line, Ruby’s most advanced authentication framework. That’s a bold claim, considering there are older and more popular authentication frameworks such as Devise, Authlogic, Sorcery and Clearance. However, hopefully by the end of this presentation, I’ll have convinced you that Rodauth is Ruby’s most advanced authentication framework. If I do a good job, maybe you’ll even want to consider using it in your next project.
Next, let’s talk about features. I’ll compare features between Devise and Rodauth. Clearance, Authlogic, and Sorcery have features that are more or less a subset of Devise’s features, so this analysis should also apply to them.
Rodauth handles pretty much all of Devise’s features. Rodauth has features for login and logout, creating accounts, verifying email addresses for accounts, handling password resets, locking out accounts after a number of failed attempts, remembering users using a cookie, logging authentication actions, expiring sessions, validating logins and passwords, and allowing users to close their accounts.
Login
Logout
Create Account
Verify Account
Reset Password
Account Lockout
Remember
Audit Logging
Session Expiration
Login/Password Validations
Close Account
Rodauth doesn’t stop there, it handles many other security related features. It supports disabling the reuse of passwords, disabling common passwords, forcing password or account expiration, forcing only a single session, or allowing global logout of all sessions for a user.
Login Account Expiration
Logout Single Session
Create Account Global Logout
Verify Account
Reset Password
Account Lockout
Remember
Audit Logging
Session Expiration
Login/Password Validations
Close Account
Disable Password Reuse
Disable Common Passwords
Password Expiration
Rodauth ships with support for multiple multifactor authentication methods. It supports WebAuthn security keys, as well as TOTP, the time based one time codes. You can also setup multifactor authentication using SMS, or generate account recovery codes in case a user loses access to their normal multifactor authentication device.
Login Account Expiration
Logout Single Session
Create Account Global Logout
Verify Account WebAuthn
Reset Password TOTP
Account Lockout SMS Codes
Remember Recovery Codes
Audit Logging
Session Expiration
Login/Password Validations
Close Account
Disable Password Reuse
Disable Common Passwords
Password Expiration
Rodauth ships with support for multiple passwordless authentication methods, allowing for authentication using email links, or using a WebAuthn security key instead of a password to login.
Login Account Expiration
Logout Single Session
Create Account Global Logout
Verify Account WebAuthn
Reset Password TOTP
Account Lockout SMS Codes
Remember Recovery Codes
Audit Logging Email Authentication
Session Expiration WebAuthn Login
Login/Password Validations
Close Account
Disable Password Reuse
Disable Common Passwords
Password Expiration
Rodauth includes a JSON API for all features it ships with, storing authentication information in JSON web tokens. That’s right, every feature can be used both with standard HTML forms and also via a JSON API. The JSON API supports cross origin resource sharing, as well as separate access and refresh tokens.
Login Account Expiration
Logout Single Session
Create Account Global Logout
Verify Account WebAuthn
Reset Password TOTP
Account Lockout SMS Codes
Remember Recovery Codes
Audit Logging Email Authentication
Session Expiration WebAuthn Login
Login/Password Validations JSON API (JWT)
Close Account CORS
Disable Password Reuse Access/Refresh Tokens
Disable Common Passwords
Password Expiration
Now, Rodauth has a lot of features, but can’t you get the same features using third party extensions to Devise? In some cases, you can. There is a Devise extension to add a JSON API, a Devise extension to add TOTP support, and a Devise extension to add WebAuthn support. However, if you want to support TOTP or WebAuthn support via a JSON API, you’ll probably have to write such an extension yourself. The main benefit of having Rodauth include all of these features is that you don’t need to worry that a Rodauth upgrade will break one of the features you are using, unlike when using a third-party Devise extension.
Login Account Expiration
Logout Single Session
Create Account Global Logout
Verify Account WebAuthn
Reset Password TOTP
Account Lockout SMS Codes
Remember Recovery Codes
Audit Logging Email Authentication
Session Expiration WebAuthn Login
Login/Password Validations JSON API (JWT)
Close Account CORS
Disable Password Reuse Access/Refresh Tokens
Disable Common Passwords
Password Expiration
Hopefully I’ve demonstrated that Rodauth ships with pretty much all authentication features your application could need.
One thing Rodauth doesn’t ship with is support for OAuth. This is because Rodauth is designed for cases where you are handling the authentication, while OAuth is designed for cases where you are letting a third party handle the authentication. Many of Rodauth’s features such as changing a password or locking out an account would make no sense when using OAuth. You can certainly use both Rodauth and OAuth in the same application if you want to support both. The authentication flows are separate and the two should not conflict.
If you do want to have OAuth support integrated into Rodauth, there is an Rodauth feature that ships in an external gem. It’s currently named roda-oauth, but will be switching to rodauth-oauth in the next release.
Next, I’m going to talk about security. Rodauth has a stronger security approach than other authentication frameworks, especially in regards to password hash storage.
The biggest problems with passwords is that people tend to choose poor passwords and tend to reuse the same password on multiple sites. This means that any popular site that stores passwords is a target for hackers. Even if the site stores nothing else of value, if hackers can get access to the stored password hashes, they can try to crack the hashes, and then use the same email address and cracked password to access the other, higher value websites.
Most of the other authentication frameworks I mentioned store the password hash in a column in the user’s table. With a single SQL injection vulnerability in the application, the hackers can get access to the password hashes for all users, which then they can try to crack offline using dedicated password cracking machines.
Rodauth uses a different approach by default. It stores the password hashes in a separate table and prevents the application from reading from that table. To check for a valid password, Rodauth calls a database function to get the password hash salt for an account. It uses the salt and provided password to compute a password hash. It then calls another database function to check if the computed password hash matches the stored password hash. The database functions have access to read the table, but direct access is not allowed. This uses a technique called security definer database functions. Rodauth uses this approach by default on PostgreSQL, MySQL, and Microsoft SQL Server. However, for compatibility, Rodauth can also function in cases where database functions cannot be used, or when the password hash is stored in a separate column in the same table.
Unlike other authentication frameworks that use purely random tokens for things like password resets and account unlocks, Rodauth’s tokens always include the account identifier in the token. This prevents the ability to brute force all current tokens for all accounts at the same time. With Rodauth, you can only try to brute force the token for a single account, and the token is compared using a constant-time string comparison. With other authentication frameworks, the token is looked up using a database index, allowing for a theoretical timing attack.
Rodauth supports and recommends using HMACs for additional security for tokens. This makes it so that even if an attacker is able to exploit an SQL injection vulnerability in the application and get access to all of the tokens, the tokens will not be usable without knowledge of a separate secret, which isn’t stored in the database.
Rodauth has three goals: security, simplicity, and flexibility. I’ve discussed security, so let me move on to simplicity. With most of the other authentication frameworks, configuring them requires changes in multiple separate areas. You’ll be configuing a model, a controller, views, and routes, all in separate places.
Rodauth handles all authentication configuration in the same block.
In your application, when you load Rodauth, you pass a block, and this block is used to configure all of Rodauth’s features. Note that all of the features I discussed earlier are optional. With Rodauth, you explicitly enable every feature you want to use.
plugin :rodauth do
end
You enable rodauth features by calling the enable method inside the block. This loads the feature into the Rodauth configuration, including loading methods used to configure the feature.
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password end
Maybe you were using a different table name for the table storing the password reset tokens. Also, maybe you want to change the subject of the password reset email. After loading the reset password feature, you can call methods to override the default values for both settings. Rodauth ships with over 1000 such configuration methods, allowing overriding virtually all parts of every supported feature.
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
reset_password_table :password_resets
reset_password_email_subject \
'You Forgot Your Password!?' end
Rodauth doesn’t stop there. It also allows you to override all parts of every supported feature in a request specific manner. All of Rodauth’s configuration methods accept a block. Passing a block allows for request specific behavior. In this example, we assume that the accounts table has a column specifying whether the account is an admin. If the account is an admin account, we use a different email subject that includes the IP address the password reset request was received from, as the admin may find that useful. Let me state again for emphesis, all of Rodauth’s configuration methods support this feature, and so all configuration can be request-specific.
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
reset_password_table :password_resets
reset_password_email_subject do
if account[:admin]
"Password Reset from #{request.ip}"
else
'You Forgot Your Password!?'
end
end end
This ability to choose exactly which features to enable and the ability to override any part of the framework in a request specific manner is how Rodauth achieves its third goal of flexibility.
When I was going through the previous example, you might have noticed that we loaded Rodauth by calling the plugin method. What’s going on here?
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
end
This is because Rodauth is implemented as a plugin for the Roda web toolkit. So far, I’ve only shown how Rodauth is configured, not how to use that. Let’s correct now.
class App < Roda
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
end
end
Roda is a web toolkit that uses the idea of routing requests using a tree made out of Ruby blocks. You can also think of it as a fast and configurable web framework. When a request is received, this route block is called.
class App < Roda
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
end
route do |r|
r.rodauth
rodauth.require_authentication
# rest of application
endend
The route block is yielded the request object, and the first line calls the rodauth method on the request object. This handles all of the routes added by Rodauth.
class App < Roda
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
end
route do |r|
r.rodauth
rodauth.require_authentication
# rest of application
end
end
The second line calls the require_authentication method on the rodauth object, which will redirect to the login page if the account is not authenticated. This ensures the entire rest of the application is only reached for sessions that have already been authenticated.
class App < Roda
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
end
route do |r|
r.rodauth
rodauth.require_authentication
# rest of application
end
end
While Roda is the fastest Ruby web framework with any significant usage, it’s still far less popular than Rails or Sinatra. So having Rodauth only usable in Roda applications would be a shame.
class App < Roda
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
end
route do |r|
r.rodauth
rodauth.require_authentication
# rest of application
end
end
Thankfully, Roda includes a middleware plugin, which allows the Roda application to be used as Rack middleware by any other Rack application, including Sinatra or Rails.
class App < Roda
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
end
plugin :middleware
route do |r|
r.rodauth
rodauth.require_authentication
# rest of application
end
end
When using Rodauth as middleware, it’s best to set the rodauth instance in the rack environment, so that the application using the Rodauth middleware will have access to the rodauth instance.
class App < Roda
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
end
plugin :middleware
route do |r|
r.rodauth
rodauth.require_authentication
env['rodauth'] = rodauth
# rest of application
end
end
With Sinatra, the integration is almost as simple as this. For Rails, it used to be substantially more involved.
class App < Roda
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
end
plugin :middleware
route do |r|
r.rodauth
rodauth.require_authentication
env['rodauth'] = rodauth
# rest of application
end
end
Thankfully, Janko Marohnic, the author of Shrine, recently released rodauth-rails, which integrates Rodauth with Rails. So if you have a Rails application and are considering authentication options, adding Rodauth is now about the same difficulty as adding one of the other authentication frameworks. In my biased opinion, it’s probably worth it if you need features that are not provided in other frameworks, or value the additional security or flexibility Rodauth offers.
In the previous example, we required authentication for all actions. What if only some sections of your application require authentication? For example, maybe most of your application is open, but only the admin section needs to be authenticated?
This is where Roda’s routing tree shines. You can easily easily move that require_authentication call from the top of the main route block to the top of the block used for the admin branch. Requests for other branches of the routing tree will not call the block, and therefore authentication will not be enforced for them.
class App < Roda
plugin :rodauth do
enable :login, :logout,
:create_account, :reset_password
end
route do |r|
r.rodauth
r.on "admin" do
rodauth.require_authentication
# admin branch
end
# rest of application
end
end
How about cases where you are need multiple different types of authentication in the same application. Maybe you are issuing WebAuthn security keys to all admins and want to enforce their use. For users, they have an option of using WebAuthn or TOTP authentication if they choose to, but they are not required to set it up.
Let’s walk through how to set this up in Rodauth.
class App < Roda
plugin :rodauth do
enable :login, :logout, :webauthn, :otp
end
plugin :rodauth, name: :admin do
enable :login, :logout, :webauthn
prefix "/admin"
session_key 'admin_id'
end
route do |r|
r.on "admin" do
r.rodauth(:admin)
rodauth(:admin).require_authentication
rodauth(:admin).require_two_factor_setup
# admin branch
end
r.rodauth
rodauth.require_authentication
# rest of application
end
end
Our Rodauth configuration for normal users stays mostly the same, except in this example we are using the webauthn and otp features.
class App < Roda
plugin :rodauth do
enable :login, :logout, :webauthn, :otp
end plugin :rodauth, name: :admin do
enable :login, :logout, :webauthn
prefix "/admin"
session_key 'admin_id'
end
route do |r|
r.on "admin" do
r.rodauth(:admin)
rodauth(:admin).require_authentication
rodauth(:admin).require_two_factor_setup
# admin branch
end
r.rodauth
rodauth.require_authentication
# rest of application
end
end
Here we have a separate configuration for admins that enables the webauthn feature, but not the otp feature.
class App < Roda
plugin :rodauth do
enable :login, :logout, :webauthn, :otp
end
plugin :rodauth, name: :admin do
enable :login, :logout, :webauthn
prefix "/admin"
session_key 'admin_id'
end
route do |r|
r.on "admin" do
r.rodauth(:admin)
rodauth(:admin).require_authentication
rodauth(:admin).require_two_factor_setup
# admin branch
end
r.rodauth
rodauth.require_authentication
# rest of application
end
end
We know this configuration is designed for admins because we have given the name admin.
class App < Roda
plugin :rodauth do
enable :login, :logout, :webauthn, :otp
end
plugin :rodauth, name: :admin do
enable :login, :logout, :webauthn
prefix "/admin"
session_key 'admin_id'
end
route do |r|
r.on "admin" do
r.rodauth(:admin)
rodauth(:admin).require_authentication
rodauth(:admin).require_two_factor_setup
# admin branch
end
r.rodauth
rodauth.require_authentication
# rest of application
end
end
In our admin configuration, we use the prefix configuration method to tell Rodauth that the routes for the admin configuration will be served under the admin branch. It’s important that if we have multiple Rodauth configurations that the Rodauth routes use different prefixes so there are no routing conflicts.
class App < Roda
plugin :rodauth do
enable :login, :logout, :webauthn, :otp
end
plugin :rodauth, name: :admin do
enable :login, :logout, :webauthn
prefix "/admin"
session_key 'admin_id'
end
route do |r|
r.on "admin" do
r.rodauth(:admin)
rodauth(:admin).require_authentication
rodauth(:admin).require_two_factor_setup
# admin branch
end
r.rodauth
rodauth.require_authentication
# rest of application
end
end
We’ll also use a different session key in the admin configuration, so that the session key is different from the default. That way logging in as a regular user will not provide access as an admin.
class App < Roda
plugin :rodauth do
enable :login, :logout, :webauthn, :otp
end
plugin :rodauth, name: :admin do
enable :login, :logout, :webauthn
prefix "/admin"
session_key 'admin_id'
end
route do |r|
r.on "admin" do
r.rodauth(:admin)
rodauth(:admin).require_authentication
rodauth(:admin).require_two_factor_setup
# admin branch
end
r.rodauth
rodauth.require_authentication
# rest of application
end
end
In the admin branch of the routing tree, we first try the Rodauth routes for the admin configuration. We then require authentication for the routes. As we have loaded a multifactor authentication feature, when require authentication is called, it actually does two checks. If the user is not logged in, it redirects them to the login page. If the user has logged in via password but has not authenticated with a second factor, it redirects them to the multifactor authentication page. So if you use require_authentication, you can be sure that you are enforcing multifactor authentication if the user has enabled it.
class App < Roda
plugin :rodauth do
enable :login, :logout, :webauthn, :otp
end
plugin :rodauth, name: :admin do
enable :login, :logout, :webauthn
prefix "/admin"
session_key 'admin_id'
end
route do |r|
r.on "admin" do
r.rodauth(:admin)
rodauth(:admin).require_authentication rodauth(:admin).require_two_factor_setup
# admin branch
end
r.rodauth
rodauth.require_authentication
# rest of application
end
end
Right after that, we call require_two_factor_setup. If the user has not setup multifactor authentication, this will redirect to the page to setup their WebAuthn security key, and they will not be able to access the admin section until they have done so.
class App < Roda
plugin :rodauth do
enable :login, :logout, :webauthn, :otp
end
plugin :rodauth, name: :admin do
enable :login, :logout, :webauthn
prefix "/admin"
session_key 'admin_id'
end
route do |r|
r.on "admin" do
r.rodauth(:admin)
rodauth(:admin).require_authentication
rodauth(:admin).require_two_factor_setup
# admin branch
end
r.rodauth
rodauth.require_authentication
# rest of application
end
end
For all other sections of the app other than the admin section, we have the same code I showed previously, which will redirect the user to the login page if they haven’t logged in. If they have setup multifactor authentication but not yet authenticated using a second factor, this will redirect them to the multifactor authentication page, just as it did for admins. However, unlike for admins, we are not forcing users to setup multifactor authentication.
class App < Roda
plugin :rodauth do
enable :login, :logout, :webauthn, :otp
end
plugin :rodauth, name: :admin do
enable :login, :logout, :webauthn
prefix "/admin"
session_key 'admin_id'
end
route do |r|
r.on "admin" do
r.rodauth(:admin)
rodauth(:admin).require_authentication
rodauth(:admin).require_two_factor_setup
# admin branch
end
r.rodauth
rodauth.require_authentication # rest of application
end
end
I hope I have convinced all of you that Rodauth is Ruby’s most advanced authentication framework. It’s even better if you will now consider using Rodauth for the authentication needs in your next application.
If you want to play around with Rodauth, try the demo-site, graciously hosted by Heroku. It’s not much to look at, but will give you a good idea for how Rodauth works from the user’s perspective.
If you’d like to learn more about Rodauth, visit the Rodauth website, which includes all of Rodauth’s documentation, including documentation of every configuration method supported by Rodauth.
If you would like to get involved with Rodauth, you can view the source on GitHub, and submit issue reports for any bugs you find, or pull requests to fix bugs or add new features.
That concludes my presentation. I would like to thank all of you for listening to me.
If you have any questions, please ask them now.