Rodauth, A++ Authentication for Ruby and Rails
This article doesn't aim to provide a step-by-step guide on how to integrate Rodauth into your Rails application, as there are already plenty of guides that cover that topic. Instead, I will explore the intricate nature of authentication and explain why Rodauth provides an effective solution. By delving deeper into the complexities of authentication processes, we can gain a better understanding of how Rodauth successfully tackles these challenges.
The Rails way
Within the Rails community, we have witnessed a series of transitions and advancements. We have evolved from Bootstrap to Tailwind, replacing JQuery with Backbone and later adopting Stimulus for front-end development. Our deployment practices have seen shifts from Vlad the Deployer and Capistrano to the utilization of Docker and MRSK, providing more flexibility and efficiency. In terms of background processing, we have embraced the power of Sidekiq, moving away from Delayed Job and Rescue. These continuous improvements reflect Rails community commitment to streamlining workflows, enhancing our technology stack and productivity.
While we have made significant progress in various aspects of our development stack, one area that continues to present challenges is authentication. Despite the availability of different libraries and approaches, finding a comprehensive solution that seamlessly integrates with Rails applications remains elusive. We have explored various authentication options, including Devise, Authlogic, and Clearance, but each has its limitations and trade-offs. As we strive for a more unified and robust authentication system, we recognize the need for a solution that provides security, flexibility, and ease of implementation.
For authentication, I've always wanted to find a solution that satisfies several simple standards: modularity, reusability, security, and decoupling from Rails. Transitioning to a standardized approach eliminates unnecessary repetition and the need to rewrite the entire code. Of course, one might say, 'What's the big deal? It's just a login and password input form.' But it's like an iceberg— the form is just the tip, while the complexity lies beneath the surface. There are numerous features that need to be implemented each time, and the scope is quite significant.
Below is a not complete list of features related to authentication:
For authentication, I've always wanted to find a solution that satisfies several simple standards: modularity, reusability, security, and decoupling from Rails. Transitioning to a standardized approach eliminates unnecessary repetition and the need to rewrite the entire code. Of course, one might say, 'What's the big deal? It's just a login and password input form.' But it's like an iceberg— the form is just the tip, while the complexity lies beneath the surface. There are numerous features that need to be implemented each time, and the scope is quite significant.
Below is a not complete list of features related to authentication:
- Login
- Logout
- Forgot password
- Password complexity and reuse
- Verify account
- Account expiration
- 2FA (TOTP, Recovery codes, SMS)
- Tokens
- External accounts
- OAuth provider
- HTTP basic auth
- Change password
- Change login/email
- WebAuthn
- Single session
- Email authentication
- Audit logging
Brief of authentication in Ruby/Rails
Long ago, it all began with restful-authentication. Then, about ten years ago, we at JetRockets switched to authlogic, around the same time when clearance, sorcery, and devise emerged.
At some point in time, Rails introduced the has_secure_password module, which adds secure password handling to any model. It encrypts the password provided by the user and stores it in the database, as well as verifies whether the provided password is correct or not. Beyond that, the module doesn't do anything else.
For a long time, there were no new authentication solutions in Ruby. This is because most companies and developers use devise. However, we are not fond of it, and we will explain why shortly. Other developers use the remaining solutions, except for restful-authentication, which has not been maintained since 2020.
For a long time, there were no new authentication solutions in Ruby. This is because most companies and developers use devise. However, we are not fond of it, and we will explain why shortly. Other developers use the remaining solutions, except for restful-authentication, which has not been maintained since 2020.
What’s wrong with Devise?
Short answer – nothing. I asked the same question my followers in Twitter a got the same thoughts I had in mind. Devise is a great tool if you need all at once, it has extensions for each and every case and if you need authentication to be made within an hour, it can be a good choice. However you will pay for this with inability to customise it for your needs. You will be able to eject controllers and views to your code, this will give you flexibility, but at the same time there are chances to break your app after Devise upgrade. You will not be able to send different emails for registration and login change. Sometimes you will have to dive into Warden to match your application needs.
But the biggest issue for me is a number of Issues and open PRs on a GitHub. I cannot trust authentication to the tool that has open issues not touched from 2015. I do not try to blame Devise contributors, since I know this is an OpenSource and everybody can help and participate, I am OK with this, but not in such a delicate question as an Authentication.
What is a good authentication solution?
At the very least, these solutions address the authentication requirements, providing the necessary functions in our case.
Modularity is essential because you may not need all the features of a gem. For example, consider Shrine with its plugin system, which allows you to use only what you need. Modularity is present in sorcery and devise. Need 2FA? Enable it. Don't need password recovery? Disable it, and so on. Why is this missing in restful-authentication, authlogic, and clearance? Their functionality is very limited, and there is nothing to exclude. They can only check the correctness of credential input and securely store the password in the database.
The configuration is standardized and does not clutter the domain model. It is not ideal when the configuration is scattered across multiple classes or resides within the model itself. It's convenient to have everything in one file, located in a dedicated place, as it makes it easier to work with. Authlogic has an inconvenient configuration approach as it pulls all its configuration into the user and session models. On the other hand, Sidekiq and Shrine has a convenient configuration setup where all settings are contained within a single file.
The ability to extend functionality is crucial because it's impossible to anticipate every project's specific requirements, and the library's author cannot know them in advance. It's great when you can write your own plugin and avoid rewriting the library's source code. Among the authentication solutions, only Devise offers such extensibility.
And finally, Authentication library should be well maintained to leverage possible security problems to zero.
Authentication Architecture
All the authentication solutions mentioned above involve patching ActiveRecord and ActionController. As a result, your
models/user.rb
and models/session.rb
files may become bloated. Besides this, controllers may also start to look something like this:# app/controllers/application_controller.rb class ApplicationController < ActionController::Base before_action :require_user private def require_user # ... end def current_session User::Session.find end def current_user @current_user ||= current_session.record end end # app/controllers/someother_controller.rb class SomeotherController < ApplicationController skip_before_action :require_user end
If you need a user to be authenticated, you use
before_action :require_user
, and if not, you use skip_before_action :require_user
, and so on for every action. The problem here is that you can't simply go to routes.rb
and see which routes require authentication and which ones don't. You have to look into each controller.We didn't like this approach at all because the configuration is scattered throughout the application code, making it difficult to have a holistic view. You have to check everything to understand how it all fits together.
At one point in JetRockets, we even considered building a separate Rack application for authentication in our projects with mobile applications. In reality, devise does pretty much the same thing, as it is built on top of warden - a Rack authentication framework. Unfortunately, devise still ends up pulling all the configuration into the model, so they only take a half step forward.
What solution we were thinking about?
When we were developing our own authentication solution about 3-4 years ago, this is what we envisioned:
- It would be a standalone Rack application that encompasses all the necessary configurations and handles all authentication strategies and processes.
- The solution would be modular, allowing us to use only what is needed for each specific project.
- There would be a plugin system to easily extend the functionality as needed.
- The solution should be well-maintained, as authentication is one of the most critical aspects of any application. As a result, we aimed for minimal GitHub issues.
Lets take a look at short example below.
# config.ru # frozen_string_literal: true require_relative 'config/environment' class Authentication def initialize(app) @app = app end def call(env) request = Rack::Request.new(env) if request.post? && request.path == '/login' # TODO: implement login return [ 302, {'Location' => '/'}, [] ] elsif request.delete? && request.path == '/logout' # TODO: implement logout return [ 302, {'Location' => '/'}, [] ] else @app.call(env) end end end use Authentication run Rails.application Rails.application.load_server
This simple example adds authentication middleware to the Rails application and utilizes two routes
POST /login
and DELETE /logout
. Lets add a bit more logic to this example.# config.ru # frozen_string_literal: true require 'json' require_relative 'config/environment' class OAuth def initialize(app) @app = app end def call(env) if request.post && request.path == '/oauth/token' # TODO: implement OAuth token endpoint response = { access_token: '123', token_type: 'bearer', expires_in: 7200, refresh_token: '456', } return [ 200, {'Content-Type' => 'application/json'}, [ response.to_json ] ] elsif request.post && request.path == '/oauth/introspect' # TODO: implement OAuth token introspect endpoint response = { active: true, scope: 'profile email', client_id: '123', username: 'igor.alexandrov@jetrockets.com', token_type: 'bearer', exp: 1681831009 } return [ 200, {'Content-Type' => 'application/json'}, [ response.to_json ] ] end end end class Authentication def initialize(app) @app = app end def call(env) request = Rack::Request.new(env) if request.post? && request.path == '/login' # TODO: implement login return [ 302, {'Location' => '/'}, [] ] elsif request.delete? && request.path == '/logout' # TODO: implement logout return [ 302, {'Location' => '/'}, [] ] else @app.call(env) end end end use OAuth if ENV['OAUTH_ENABLED'] == true use Authentication run Rails.application Rails.application.load_server
It already looks more complex and we can try to answer questions that we stated above. “Is this Rack based?” – yes. “Is it extendable?” – yes. “Is it modular?” – no. “Is it supportable?” – no.
How we can make it better?
How we can make it better?
Microframeworks
Ruby microframeworks are lightweight frameworks designed to provide minimalistic and efficient web development solutions. These frameworks focus on simplicity and ease of use, allowing developers to quickly build web applications with minimal overhead. They typically have a small codebase and a minimal set of features, making them ideal for small-scale projects or APIs. Ruby microframeworks follow the philosophy of "Convention over Configuration," providing sensible defaults and allowing developers to focus on writing their application code rather than dealing with complex configuration. Some popular Ruby microframeworks include Sinatra, Cuba, and Roda, each offering its own unique set of features and advantages. These microframeworks provide flexibility, speed, and a streamlined development experience for Ruby developers seeking lightweight solutions.
Let’s rewrite an example above with Sinatra.
Let’s rewrite an example above with Sinatra.
# config.ru # frozen_string_literal: true require 'sinatra/base' require_relative 'config/environment' class OAuth < Sinatra::Base before do content_type :json end post '/oauth/token' do # TODO: implement OAuth token endpoint { access_token: '123', token_type: 'bearer', expires_in: 7200, refresh_token: '456', }.to_json end post '/oauth/introspect' do # TODO: implement OAuth introspection endpoint { active: true, scope: 'profile email', client_id: '123', username: 'igor.alexandrov@jetrockets.com', token_type: 'bearer', exp: 1681831009 } end end class Authentication < Sinatra::Base post '/login' do # TODO: implement login redirect '/' end delete '/logout' do # TODO: implement logout redirect '/' end end use OAuth if ENV['OAUTH_ENABLED'] == true use Authentication run Rails.application Rails.application.load_server
The utilization of Sinatra in the given example presents notable improvements when compared to a basic Rack application. Notably, Sinatra offers routing capabilities and helpful utilities for handling various request types. However, it is important to acknowledge the limitations of Sinatra. Despite its perceived performance advantages, empirical benchmarks, such as the R10K benchmark, have indicated that it may not outperform Rails 7, particularly when dealing with a significant number of routes. Additionally, the absence of native support for block syntax in Sinatra routing can result in lengthy and cumbersome route declarations, detracting from the overall usability of the framework.
One plausible rationale behind Jeremy Evans' decision to fork the Ruby framework Cuba and embark on the development of Roda may have been the limitations encountered with Sinatra. Notably, Roda emerged as a highly commendable framework, distinguished by its impressive performance, built-in routing tree functionality, extensible plugin interface, and a deliberate focus on immutability in its design. Such qualities position Roda as an optimal candidate for constructing robust authentication library, owing to its ability to deliver efficient and flexible solutions in this domain. Lets try to rewrite an example above with Roda.
One plausible rationale behind Jeremy Evans' decision to fork the Ruby framework Cuba and embark on the development of Roda may have been the limitations encountered with Sinatra. Notably, Roda emerged as a highly commendable framework, distinguished by its impressive performance, built-in routing tree functionality, extensible plugin interface, and a deliberate focus on immutability in its design. Such qualities position Roda as an optimal candidate for constructing robust authentication library, owing to its ability to deliver efficient and flexible solutions in this domain. Lets try to rewrite an example above with Roda.
# config.ru # frozen_string_literal: true require "roda" require_relative "config/environment" class Authentication < Roda plugin :json plugin :all_verbs route do |r| r.post 'login' do # TODO: implement login r.redirect '/' end r.delete 'logout' do # TODO: implement logout r.redirect '/' end if ENV['OAUTH_ENABLED'] == true r.on 'oauth' do r.get 'token' do # TODO: implement OAuth token endpoint { access_token: '123', token_type: 'bearer', expires_in: 7200, refresh_token: '456', } end r.post 'introspect' do # TODO: implement OAuth introspection endpoint { active: true, scope: 'profile email', client_id: '123', username: 'igor.alexandrov@jetrockets.com', token_type: 'bearer', exp: 1681831009 } end end end end end use Authentication run Rails.application Rails.application.load_server
So what is Rodauth?
Rodauth is a standalone Ruby application that serves only one goal – provide authentication, you should always keep this in mind when you work it. Being a standalone application makes easier to test Rodauth, which makes chances to make mistakes and have security issues less compared to its competitors.
To make it more understandable lets rewrite an example above with Rodauth.
To make it more understandable lets rewrite an example above with Rodauth.
# config.ru # frozen_string_literal: true require_relative "config/environment" require "roda" class Authentication < Roda plugin :json plugin :all_verbs plugin :middleware plugin :sessions, secret: "some really strong strong strong strong strong strong secret goes here" plugin :rodauth do enable :login, :logout end route do |r| r.rodauth env['rodauth'] = rodauth if ENV['OAUTH_ENABLED'] == true # OAuth logic goes here # ... end end end use Authentication run Rails.application Rails.application.load_server
As you may notice instead of defining routes for
The remarkable aspect of Rodauth extends beyond its appealing aesthetics, as it boasts an extensive repertoire of features that facilitate the configuration of authentication mechanisms for a wide range of projects. From account creation and verification to login workflows, password complexity enforcement, password reset functionalities, and even support for two-factor authentication (2FA), Rodauth offers an inclusive set of capabilities right out of the box.
login
and logout
we use rodauth
plugin for Roda that does all the magic inside.The remarkable aspect of Rodauth extends beyond its appealing aesthetics, as it boasts an extensive repertoire of features that facilitate the configuration of authentication mechanisms for a wide range of projects. From account creation and verification to login workflows, password complexity enforcement, password reset functionalities, and even support for two-factor authentication (2FA), Rodauth offers an inclusive set of capabilities right out of the box.
The most notable attribute of Rodauth lies in its ability to decouple authentication from the core business logic of an application. However, this very feature presented a significant challenge when attempting to integrate Rodauth into Rails frameworks. Internally, Rodauth relies on Sequel for database interactions, necessitating the establishment of two separate database connections—one for ActiveRecord and another for Sequel. Moreover, Rodauth lacked seamless integration with Rails views, limited support for features such as flash messages, CSRF protection, HMAC security, and various other components that are inherent to the Rails ecosystem. As a result, while Rodauth remained functional, it fell short of being a comprehensive library tailored specifically for Rails development.
Rails Integration
When discussing the integration of Rodauth with Rails, it's essential to understand the underlying structure of a typical Ruby app's Rack stack. The Rack stack consists of multiple Rack applications, known as middlewares. It's important to note that Rails, by design, is not strictly a monolithic application. It can be extended using standardized modules called Rack middlewares. Even the routing in Rails is not inherently part of the monolith; it functions as a middleware that sits in the stack just before the application's business logic. Preceding the routing, there are numerous middlewares responsible for handling various aspects of the HTTP specification, such as cookies, sessions, and CORS. These functionalities are defined in Rack and ActionDispatch. Each middleware has the ability to handle requests, modify them, or even interrupt and return a response. The diagram below illustrates this concept. Since Rodauth is a Rack based application it can be easily added the the stack and our schema will look like the one below. So, what does this mean for developers? With Rodauth integrated into your application, it has the power to handle your application's requests, modify them, and return appropriate responses. Let's consider a scenario where your application has a
But what if user is authenticated and we need to use his accounts details somewhere in the template? This were the biggest problem existed. Lets come back to the
/profile
route that should only be accessible to users with an active session. With Rodauth, you can configure it to check if the request path is /profile
and if no session information is provided. In this case, Rodauth can respond with an HTTP 302 status code and redirect the user to the /login
path, which is also handled by Rodauth. The best part? Our Rails application, our core business logic, won't even be touched. Isn't that great?But what if user is authenticated and we need to use his accounts details somewhere in the template? This were the biggest problem existed. Lets come back to the
Authentication
class we defined above.require "roda" class Authentication < Roda # ... route do |r| r.rodauth env['rodauth'] = rodauth # ... end end use Authentication
As you may notice we assigned
rodauth
to env['rodauth']
to be able to use Rodauth methods in our application later.# app/controllers/application_controller.rb class ApplicationController < ActionController::Base # ... private def current_account return nil unless request.env['rodauth'].account! Account.instantiate request.env['rodauth'].account!&.stringify_keys end helper_method :current_account # ... end
The same approach could be used in templates.
# app/views/application.html.erb <% if request.env['rodauth'].logged_in %> <%= current_account.email %> <% else %> <% link_to "Log In", request.env['rodauth'].login_path %> <% end %>
Such integration of Rodauth with Rails has proven to be effective and provides a solid foundation for your application. However, prior to the introduction of rodauth-rails, you had to address certain aspects such as CSRF protection, layout rendering, and various other tasks on your own. This all changed with the development of rodauth-rails. Janko Marohnić, the renowned author of the Shrine library mentioned multiple times in this article, has created an integration library that seamlessly combines the strengths of Rails (simple installation and developer-friendly environment) with the power of Rodauth. The result is a versatile authentication solution for Rails, akin to a Swiss Army knife.
Conclusions
Throughout this article, we have explored why Rodauth stands out among other authentication libraries.
Firstly, Rodauth provides a comprehensive set of features out of the box, including secure password hashing, multi-factor authentication, account locking, and more. This eliminates the need for developers to spend valuable time and effort implementing these features from scratch.
Additionally, Rodauth offers excellent flexibility and extensibility. It allows for easy customization and integration with existing Rails applications, allowing developers to tailor the authentication system to their specific requirements. The flexibility also extends to the authentication flow, enabling seamless integration with various third-party services.
Firstly, Rodauth provides a comprehensive set of features out of the box, including secure password hashing, multi-factor authentication, account locking, and more. This eliminates the need for developers to spend valuable time and effort implementing these features from scratch.
Additionally, Rodauth offers excellent flexibility and extensibility. It allows for easy customization and integration with existing Rails applications, allowing developers to tailor the authentication system to their specific requirements. The flexibility also extends to the authentication flow, enabling seamless integration with various third-party services.
Moreover, Rodauth emphasizes security and adheres to best practices in authentication. It handles potential vulnerabilities such as session fixation attacks, brute force protection, and secure password handling. By leveraging Rodauth, Rails applications can benefit from a robust and reliable authentication system that safeguards user data and protects against potential threats.
With regular updates and contributions from a community of developers, the library continues to improve and evolve, ensuring it remains updated with the latest security standards and industry trends.
Overall, Rodauth offers a powerful and feature-rich authentication solution for Rails applications. Its ease of integration, flexibility, security focus, and community support make it a compelling choice as the default authentication solution in Rails. By adopting Rodauth, developers can save time, enhance security, and streamline the authentication process, allowing them to focus on building other core features of their applications.
Discover More Reads
Categories: