DEV Community

David Boureau
David Boureau

Posted on • Originally published at bootrails.com

Authentication vs Authorization with Rails 7

Article originally published here : https://bootrails.com/blog/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

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
Enter fullscreen mode Exit fullscreen mode

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)

rails new myapp
cd myapp 
bundle add authentication-zero
bin/rails generate authentication

Enter fullscreen mode Exit fullscreen mode

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

Seed and create the database

Inside db/seeds.rb

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

Now create the database


 # Create database and schema.rb
 bin/rails db:create
 bin/rails db:migrate
 bin/rails db:seed

Enter fullscreen mode Exit fullscreen mode

And launch the server :

 # Launch the local web server
 bin/rails server
Enter fullscreen mode Exit fullscreen mode

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 :

 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

Enter fullscreen mode Exit fullscreen mode

Profile page like this :

 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

Enter fullscreen mode Exit fullscreen mode

And an admin page like this :

 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

Enter fullscreen mode Exit fullscreen mode

Now apply routes to these new pages :

 # 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
Enter fullscreen mode Exit fullscreen mode

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

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
{: .prompt-tip }

Make a page available to the public

Modify the HomeController as follow :

# Inside app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController

 skip_before_action :authenticate # add this

end
Enter fullscreen mode Exit fullscreen mode

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 :

 bin/rails generate migration add_role_to_users role:string
Enter fullscreen mode Exit fullscreen mode

Modify the migration file as follow :

class AddRoleToUsers < ActiveRecord::Migration
 def change
   # modify as follow
   add_column :users, :role, :string, :default => "customer"
 end
end
Enter fullscreen mode Exit fullscreen mode

And run

 bin/rails db:migrate
Enter fullscreen mode Exit fullscreen mode

Inside user.rb

class User < ApplicationRecord
 has_secure_password

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

 # ...rest of code
end
Enter fullscreen mode Exit fullscreen mode

Seed your database with various profiles

Inside db/seeds.rb

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)
Enter fullscreen mode Exit fullscreen mode

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

 bin/rails db:drop db:create db:migrate db:seed
Enter fullscreen mode Exit fullscreen mode

Relaunch your local web server with

 bin/rails server
Enter fullscreen mode Exit fullscreen mode

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.
{: .prompt-danger }

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:

gem 'pundit'
Enter fullscreen mode Exit fullscreen mode

And run

bundle install
Enter fullscreen mode Exit fullscreen mode

Include Pundit in your application controller:

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
Enter fullscreen mode Exit fullscreen mode

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

bin/rails g pundit:install
Enter fullscreen mode Exit fullscreen mode

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

Adding policies

touch app/policies/admin_policy.rb

Enter fullscreen mode Exit fullscreen mode

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

class AdminPolicy < ApplicationPolicy
 attr_reader :user

 def index?
  return user.admin?
 end
end
Enter fullscreen mode Exit fullscreen mode

Injecting Pundit policies into controller

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

class AdminController < ApplicationController

 def index
  authorize Current.user, policy_class: AdminPolicy
 end 

end
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)