Ruby on Rails: Native route constraint for authentication
Tags: rails, webdev, tutorial
Published: 2024-04-19 13:21:39

Since Rails 7, there's more and more tooling that enables us, developers, to roll our own authentication. [Devise](https://github.com/heartcombo/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:

```ruby
# routes.rb

authenticated { |user| user.admin? } do
  get :dashboard
end
```

We'll try to replicate this natively.

---

## Constraint

We'll leverage [advanced routing constraints](https://edgeguides.rubyonrails.org/routing.html#advanced-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.

```ruby
# app/constraints/authenticated_constraint.rb

class AuthenticatedConstraint
  def matches?(request)
    return true_if_authenticated_method
  end
end
```

---

### 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:

```ruby
def sign_in(user)
  Current.user = user
  cookies.encrypted.permanent[:autheticated_user_id] = user.id
end
```

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](https://www.rubydoc.info/docs/rails/ActionDispatch/Cookies/CookieJar#build-class_method).

```ruby
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
```

---

### 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.

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

def matches?(request)
  # ...

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

  # ...
end
```

---

### Voilà!

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

The finished constraint class is as follows:

```ruby
# 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
```

_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.

```ruby
# 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
```

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](https://twitter.com/predkelis) or visit my [website](https://predkelis.com)!