-
Notifications
You must be signed in to change notification settings - Fork 0
2. Users & Authentication
The first thing that we'll need to add to the application are users that can sign up and log in.
In this step we'll see how to do just that with the support of the gem (external library) devise.
The Gemfile file allows you to specify what gem dependencies are needed for your Rails application.
We can add devise (or any other gem) to our application by adding the following to the Gemfile:
...
gem "pg"
# ===========
gem 'devise'
# ===========
...And then running the following in your console:
foo@bar:~$ bundle installLastly, to finish installing devise we need to run:
foo@bar:~$ rails generate devise:installDevise by itself is not enough if we are doing an api only rails app, like we do in this workshop. In this cases, we can add the gem devise-jwt to bridge the gap.
Let's add to our Gemfile:
...
gem 'devise'
# ===========
gem 'devise-jwt'
# ===========
...and run:
foo@bar:~$ bundle installEdit the file config/initializers/devise.rb to include the following content:
# frozen_string_literal: true
Devise.setup do |config|
# ...
# Devise jwt config
config.jwt do |jwt|
jwt.secret = ENV.fetch('JWT_SECRET', nil)
end
endEdit the file .ENV to include:
JWT_SECRET=secret_signature # you can generate one with `SecureRandom.hex(32)`
We can use devise's generator to automatically create our users model and migration.
foo@bar:~$ rails generate devise user
invoke active_record
create db/migrate/YYYYMMddhhmmss_devise_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
insert app/models/user.rb
route devise_for :usersIn this workshop we leave specs (tests) as future work. We strongly advise to replace the auto-generated specs and use rspec-rails gem instead.
We will need to modify the migration generated by devise to add the jti's that will be used by devise-jwt.
To do that, let's add the following to our migration db/migrate/YYYYMMddhhmmss_devise_create_users.rb:
## Tokens
...
# ===========
t.string :jti, null: false
t.index :jti, unique: true
# ===========
t.timestamps null: false
...That's it, now you can run the migration to update your database
foo@bar:~$ rails db:migrate
== 20220818142458 DeviseCreateUsers: migrating ================================
-- create_table(:users)
-> 0.0069s
-- add_index(:users, :email, {:unique=>true})
-> 0.0019s
-- add_index(:users, :reset_password_token, {:unique=>true})
-> 0.0012s
== 20220818142458 DeviseCreateUsers: migrated (0.0100s) =======================First we create an API controller which all other controllers will inherit from:
app/controllers/api/api_controller.rb
# frozen_string_literal: true
module Api
class ApiController < ActionController::API
before_action :authenticate_user!
def route_not_found
render_generic_error('Route not found', status: :not_found)
end
end
endModify the User model to include:
class User < ApplicationRecord
# ...
include Devise::JWT::RevocationStrategies::JTIMatcher
# ...
devise :database_authenticatable, :registerable,
#...
:jwt_authenticatable, jwt_revocation_strategy: self
endSince Rails uses web sessions but devise-jwt is sessionless (stateless) , we need to tell Devise to use a fake session.
In order to do that, first we create the FakeSession model at app/controllers/concerns/fake_session.rb:
# frozen_string_literal: true
# This module is used to avoid the following error:
# "NoMethodError: undefined method `[]' for nil:NilClass"
# when using Devise with an API-only Rails app.
# This concern must be used in the controller that inherits from
# Devise::RegistrationsController when using Devise JWT.
module FakeSession
extend ActiveSupport::Concern
class FakeRackSession < Hash
def enabled?
false
end
end
included do
before_action :set_fake_rack_session_for_devise
private
def set_fake_rack_session_for_devise
request.env['rack.session'] ||= FakeRackSession.new
end
end
endThen we need to modify Devise controllers to tell it to use the fake session. To do that we need to modify:
app/controllers/api/users/passwords_controller.rbapp/controllers/api/users/registrations_controller.rbapp/controllers/api/users/sessions_controller.rb
And include:
# frozen_string_literal: true
module Api
module Users
class XXXController < Devise::XXXController
include FakeSession
respond_to :json
end
end
endOur last step is going to be overriding the routes file to tell Devise to use our controllers.
In order to do that, we need to modify config/routes.rb and make sure we have:
Rails.application.routes.draw do
scope module: 'api', defaults: { format: :json } do
devise_for :users, controllers: {
confirmations: 'api/users/confirmations',
sessions: 'api/users/sessions',
registrations: 'api/users/registrations',
passwords: 'api/users/passwords'
}
# ...We are done! We have just created our first model User and we are ready to sign up and login to our application.
By using devise and devise-jwt in our ApiController every controller that inherits from it will have access to the logged in user by using current_user.