DEV Community

Cover image for Ruby on Rails: Native route constraint for authentication
Elvinas Predkelis
Elvinas Predkelis

Posted on

Ruby on Rails: Native route constraint for authentication

Since Rails 7, there's more and more tooling that enables us, developers, to roll our own authentication. Devise is great and has been an amazing companion over the years. It also has this neat little feature - an authenticated route constraint which "hides" certain routes from people that are not signed in.

If you're using devise, you might have seen something like this:

# routes.rb

authenticated { |user| user.admin? } do
  get :dashboard
end
Enter fullscreen mode Exit fullscreen mode

We'll try to replicate this natively.


Constraint

We'll leverage advanced routing constraints to make this happen. Honestly, it's not as scary as it sounds. It basically means that we'll extract the logic to a class.

Below is a barebones class for the constraint.

# app/constraints/authenticated_constraint.rb

class AuthenticatedConstraint
  def matches?(request)
    return true_if_authenticated_method
  end
end
Enter fullscreen mode Exit fullscreen mode

How does the user authentication work?

Web applications usually leverage cookies to facilitate user authentication. When a user signs in, a cookie with a unique identifier is set in the browser. This cookie is then used to identify the user on subsequent requests.

Therefore, the signing in logic in your application might look something like this:

def sign_in(user)
  Current.user = user
  cookies.encrypted.permanent[:autheticated_user_id] = user.id
end
Enter fullscreen mode Exit fullscreen mode

Similarly, when a user signs out, the cookie is removed. Pretty simple.


Accessing the cookies

In order to figure out whether a user is signed in, we'll use the same exact cookies in the constraint.

Rails usually encrypts the cookies for security reasons. Fortunately, there's decryption tooling that helps to dip our fingers into the cookie jar.

def matches?(request)
  @cookies = ActionDispatch::Cookies::CookieJar.build(request, request.cookies)

  # And then the cookies are accessible, for example:
  @cookies.encrypted[:authenticated_user_id]

  # ...
end
Enter fullscreen mode Exit fullscreen mode

Passing a block

A neat little thing that authenticated also method does is that it allows passing a block. This block helps to add additional conditions to the constraint. For example, checking whether the user is an administrator.

In other words, we'll recreate that { |user| user.admin? } part.

def initialize(&block)
  @block = block || lambda { |user| true }
end

def matches?(request)
  # ...

  # Here we could call the supplied block
  @block.call(current_user)

  # ...
end
Enter fullscreen mode Exit fullscreen mode

Voilà!

Finally, we can mix everything together and add some finishing touches to make everything more readable.

The finished constraint class is as follows:

# app/constraints/authenticated_constraint.rb

class AuthenticatedConstraint
  def initialize(&block)
    @block = block || lambda { |user| true }
  end

  def matches?(request)
    @cookies = ActionDispatch::Cookies::CookieJar.build(request, request.cookies)
    return signed_in? && @block.call(current_user)
  end

  private

  def current_user
    @user ||= User.find(@cookies.encrypted[:authenticated_user_id])
  end

  def signed_in?
    current_user.present?
  end
end
Enter fullscreen mode Exit fullscreen mode

Obviously, it might need some adjustment to fit your application.


Implementation

Adding this constraint into action is rather straight forward as authenticated method was used as an inspiration.

# routes.rb

# Routes available to admins only
constraints AuthenticatedConstraint.new{ |user| user.admin? } do
  mount Sidekiq::Web => "/sidekiq"
  get :admin_dashboard
end

# Routes available to authenticated users
constraints AuthenticatedConstraint.new do
  get :my_profile
  root to: :dashboard, as: :user_root
end
Enter fullscreen mode Exit fullscreen mode

That's it! 🎉


Wrapping up

Hopefully, this article has shed some light on how to create a route constraint for authenticated users. It's rather useful when trying to set a new root route for authenticated users. Also, limit the access to certain routes based on user roles.


If you have any questions or suggestions, feel free to reach out to me on Twitter or visit my website!

Top comments (0)