Post

Authentication vs Authorization with Rails 7

Authorization means give or refuse access to the current User to some URLs (or more generally, any kind of resource.)

It’s closely bound to Authentication, but it’s different.

Think about an international conference about climate change.

Authentication is the entrance ticket, you can not enter the conference without the ticket.

Authorization is about checking the ticket levels of access : does it allow you to access gate A or snack B?

Authentication with Rails 7

Now that Rails seems to promote generators for authentication (it will be the case in Rails 8), let’s use the closest possibility (right now) : the auth-zero generator.

Prerequisites

Tools I will use in this tutorial

1
2
3
4
5
ruby -v # 3.3.0
rails -v # 7.1.3
bundle -v # 2.4.10
node -v # 20.9.0
git --version # 2.34.1

Before authorization, authentication

Now that Rails seems to promote generator for authentication (it will be the case in Rails 8), let’s use the closest possibility (right now) : the auth-zero generator.

So let’s start by building an authentication system (no CSS)

1
2
3
4
5
rails new myapp
cd myapp 
bundle add authentication-zero
bin/rails generate authentication

Great! So now we have a default rails app, augmented with authentication features.

Seed and create the database

Inside db/seeds.rb

1
User.create(email: "simple@user.com", password_digest: BCrypt::Password.create("Secret1*3*5*"), verified: true)

Now create the database

1
2
3
4
5
6
 
 # Create database and schema.rb
 bin/rails db:create
 bin/rails db:migrate
 bin/rails db:seed

And launch the server :

1
2
 # Launch the local web server
 bin/rails server

Good! Now open localhost:3000 and try to authenticate with the user that is inside seed.rb

Now log out, and stop the local web server.

Build 3 different pages

Now let’s build 3 different pages : home page, profile page, and admin page.

Welcome page like this :

1
2
3
4
5
 echo "class WelcomeController < ApplicationController" > app/controllers/welcome_controller.rb
 echo "end" >> app/controllers/welcome_controller.rb
 mkdir app/views/welcome
 echo '<h1>welcome page</h1>' > app/views/welcome/index.html.erb
 

Profile page like this :

1
2
3
4
5
 echo "class ProfileController < ApplicationController" > app/controllers/profile_controller.rb
 echo "end" >> app/controllers/profile_controller.rb
 mkdir app/views/profile
 echo '<h1>Profile page</h1>' > app/views/profile/index.html.erb
 

And an admin page like this :

1
2
3
4
5
 echo "class AdminController < ApplicationController" > app/controllers/admin_controller.rb
 echo "end" >> app/controllers/admin_controller.rb
 mkdir app/views/admin
 echo '<h1>Admin only area!</h1>' > app/views/admin/index.html.erb
 

Now apply routes to these new pages :

1
2
3
4
5
6
7
8
9
10
11
12
 # inside config/routes.rb
 Rails.application.routes.draw do

  # add these 3 lines
  get "welcome", to: "welcome#index"
  get "profile", to: "profile#index"
  get "admin", to: "admin#index"

  # all other routes remain unchanged
  # ...

 end

Relaunch your local web server, and try to access to the following URLs:

  • “http://locahost:3000/welcome”, welcome page
  • “http://locahost:3000/profile”, profile page
  • “http://locahost:3000/admin”, admin-only area ;)

Yikes! None of them is available, unless you’ve gone through the login screen.

A good practice I can see in many Rails project is to make every routes “authenticated” by default, which mean you cannot access them, unless you are already authenticated

Make a page available to the public

Modify the HomeController as follow :

1
2
3
4
5
6
# Inside app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController

 skip_before_action :authenticate # add this

end

Ok sounds fair. You will barely see any welcome page that needs authentication.

So now open the welcome page again (http://locahost:3000/welcome), you should be able to access it without the need of authentication.

Add role column

Remember, authorization is linked to authentication, but is not exactly the same thing.

  • A simple visitor can view the home page, but can not access the user profile page
  • A user can access home page and profile page, but can not access the admin dashboard
  • An admin may access every part of the application

So we need a “role” column now, in order to know who has access to what

Stop your local web server

Back to your terminal, enter :

1
 bin/rails generate migration add_role_to_users role:string

Modify the migration file as follow :

1
2
3
4
5
6
class AddRoleToUsers < ActiveRecord::Migration
 def change
   # modify as follow
   add_column :users, :role, :string, :default => "customer"
 end
end

And run

1
 bin/rails db:migrate

Inside user.rb

1
2
3
4
5
6
7
8
class User < ApplicationRecord
 has_secure_password

 # add this line
 enum role: {customer: "customer", admin: "admin"}

 # ...rest of code
end

Seed your database with various profiles

Inside db/seeds.rb

1
2
3
User.create(email: "simple@user.com", role: 'customer', password_digest: BCrypt::Password.create("Secret1*3*5*"), verified: true)
User.create(email: "customer@user.com", role: 'customer', password_digest: BCrypt::Password.create("Secret1*3*5*"), verified: true)
User.create(email: "admin@user.com", role: 'admin', password_digest: BCrypt::Password.create("Secret1*3*5*"), verified: true)

Recreate your database (you won’t usually do this once you have a production database, but for a small tutorial, that’s ok)

1
 bin/rails db:drop db:create db:migrate db:seed

Relaunch your local web server with

1
 bin/rails server

And connect as customer@user.com, and try to access to localhost:3000/admin

Yikes! Even if we connect as customers, we have access to the admin area. We should have raised an authorization error, so now it’s time for authorization with Pundit.

Rails 7 authorization with Pundit (finally!)

It’s time to add Pundit to clearly prevent unauthorized users from going everywhere inside our flashy webapp.

Add gem 'pundit' to Gemfile:

1
gem 'pundit'

And run

1
bundle install

Include Pundit in your application controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ApplicationController < ActionController::Base
  include Pundit::Authorization # add this line

 # was here before, left as-is
 before_action :set_current_request_details
 before_action :authenticate

 # add this method, required by Pundit
 def current_user 
  Current.user # this belongs to the authentication mechanism, so not Pundit
 end

 # rest of code ...

end

Also, you can run the generator to set up an application policy with some useful defaults:

1
bin/rails g pundit:install

After that, you need to restart the Rails server. Now Rails can pick up any classes in the new app/policies/ directory.

Adding policies

1
2
touch app/policies/admin_policy.rb

Our admin_policy.rb only allows admin to go to the admin dashboard:

1
2
3
4
5
6
7
class AdminPolicy < ApplicationPolicy
 attr_reader :user

 def index?
  return user.admin?
 end
end

Injecting Pundit policies into controller

So now let’s inject our policy into the controller.

1
2
3
4
5
6
7
class AdminController < ApplicationController

 def index
  authorize Current.user, policy_class: AdminPolicy
 end 

end

Yay! Not too much Rails magic here. We explicitly authorize the current user for a given policy.

Now relaunch your web server, and try to connect as a customer to the admin area. It should raise an error.

Try to connect as admin to the admin area. It should work properly :)

How good is that ?

Summary

We covered 2 major concept of any web app, from scratch :

  • Authentication, with a generator : Rails 8 will work like this, so we relied on a similar gem to achieve it.
  • Authorization, with a well-known gem, in order to know which user has access to which resource.

Have a good day then :)

David

This post is licensed under CC BY 4.0 by the author.