Devise API Authentication | Ruby on Rails 7
Tutorial
[!
Vidéo de base :
Création de l’API 🛤
Création d’une app Rails en mode API (donc sans front)
rails new my_api --api
Rajout de 3 gems : devise, devise-jwt, et rack-cors
bundle add devise devise-jwt rack-cors
Devise sert au setup de tout le système d’authentification en tant que tel
Devise-jwt est une extension de Devise permettant d’utiliser les JWT token pour
l’authentification
Rack CORS permet de faire des requêtes cross-domains (en gros de pouvoir faire
des requêtes à l'API depuis un autre domaine)
Configuration de Rack CORS
C’est parti pour quelques modifications dans le fichier config/initializers/[Link]
Ces changements permettent d’autoriser n’importe quel site à faire des requêtes à l’API,
pour autoriser une seule origine➡️
origins "[url]"
# config/initializers/[Link]
# Be sure to restart your server when you modify this file.
# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin
AJAX requests.
# Read more: [Link]
[Link].insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*',
headers: :any,
methods: %i[get post put patch delete options head],
expose: %w[Authorization Uid]
end
end
Installation de Devise et génération de la table User
rails g devise:install
rails g devise User
Devise JWT 💲
Génération de la Denylist
La DenyList est une méthode révocation de Token JWT, en gros à chaque fois qu'un
utilisateur se déconnecte ou que le token est expiré un nouveau token sera généré pour
cet utilisateur
rails g model jwt_denylist jti:string exp:datetime
Le jti est l’identifiant unique d’un token
Exp contient sa date d’expiration
⚠️Pour que tout fonctionne, vous devez renommer le fichier de migration
(de [timestamp]_create_jwt_denylists.rb à
[timestamp]_create_jwt_denylist.rb), la classe et la table au singulier (voir
en-dessous)
Fichier de migration :
# db/migrate/20220228223034_create_jwt_denylist.rb
class CreateJwtDenylist < ActiveRecord::Migration[7.0]
def change
create_table :jwt_denylist do |t|
[Link] :jti, null: false
[Link] :exp, null: false
[Link]
end
add_index :jwt_denylist, :jti
end
end
Petit changements au modèle User
:jwt_authenticatable permet de dire à Devise que User utilise jwt pour
l’authentification
:jwt_revocation_strategy permet de dire à User comment il doit révoquer les
tokens, et qu’il doit utiliser le modèle JwtDenylist pour ça
# app/models/[Link]
class User < ApplicationRecord
# Il faut ajouter les deux modules commençant par jwt
devise :database_authenticatable, :registerable,
:jwt_authenticatable,
jwt_revocation_strategy: JwtDenylist
end
Petits ajouts des familles au Model JwtDenylist
On indique aussi au modèle JwtDenylist qu’il doit utiliser la stratégie de révocation
denylist (oui oui)
# app/models/jwt_denylist.rb
class JwtDenylist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Denylist
self.table_name = 'jwt_denylist'
end
rails db:migrate 🙂
Devise API JWT Controllers for Sessions and
Registrations 🧒
Créer le fichier members_controller.rb
La méthode show permettra de s’authentifier avec un token au lieu d’avec l’email et le
password
# app/controllers/members_controller.rb
class MembersController < ApplicationController
before_action :authenticate_user!
def show
user = get_user_from_token
render json: {
message: "If you see this, you're in!",
user: user
end
private
def get_user_from_token
jwt_payload = [Link]([Link]['Authorization'].split(' ')[1],
[Link][:jwt_secret_key]).first
user_id = jwt_payload['sub']
[Link](user_id.to_s)
end
end
Création des Users
Deux nouveaux controllers à créer, qui modifieront les controllers de registration et de
session de Devise
⚠️Il faut créer ces fichiers dans un dossier users dans app/controllers (voir
le commentaire en haut des snippets)
# app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
respond_to :json
private
def respond_with(resource, _opts = {})
register_success && return if [Link]?
register_failed
end
def register_success
render json: {
message: 'Signed up sucessfully.',
user: current_user
}, status: :ok
end
def register_failed
render json: { message: 'Something went wrong.' }, status:
:unprocessable_entity
end
end
# app/controllers/users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
respond_to :json
private
def respond_with(_resource, _opts = {})
render json: {
message: 'You are logged in.',
user: current_user
}, status: :ok
end
def respond_to_on_destroy
log_out_success && return if current_user
log_out_failure
end
def log_out_success
render json: { message: 'You are logged out.' }, status: :ok
end
def log_out_failure
render json: { message: 'Hmm nothing happened.' }, status: :unauthorized
end
end
Devise JWT Secret Key 🔑
Config du secret JWT utilisé pour décoder les tokens dans
config/initializers/[Link]
[Link] do |config|
# Plein de code
[Link] do |jwt|
[Link] =
[Link][:jwt_secret_key]
end
# Encore tout plein de code
end
1. Génération du secret
rake secret
Copie de la string générée
EDITOR=nano rails credentials:edit
Ajout en bas du fichier de :
devise:
jwt_secret_key: [clé copiée] // ⚠ Il faut mettre 2 espaces au début de
cette ligne
Routes 🛣
Go config/[Link]
# config/[Link]
[Link] do
devise_for :users,
controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations'
get '/member-data', to: 'members#show'
# Define your application routes per the DSL in
[Link]
# Defines the root path route ("/")
# root "articles#index"
end
Configure Session Store in Rails 7
1. Config pour utiliser les cookies dans config/[Link]
# config/[Link]
module DeviseVue
class Application < Rails::Application
# Du code cool
# This also configures session_options for use below
config.session_store :cookie_store, key: '_interslice_session'
# Required for all session management (regardless of session_store)
[Link] ActionDispatch::Cookies
[Link] config.session_store, config.session_options
# Plein de code
end
end
Et voilà ! 🎉
Et les routes c’est quoi ?
Register
POST /users
Données attendues :
"user": {
"email": string,
"password": string
Pour la tester :
curl -XPOST -H "Content-Type: application/json" -d '{ "user": { "email":
"test@[Link]", "password": "12345678" } }' [Link]
Réponse :
=> {"message":"Signed up successfully.","user":{"id":
[id],"email":"test@[Link]","created_at":[timestamp],"updated_at":
[timestamp]}
Login
POST /users/sign_in
Données attendues
"user": {
"email": string,
"password": string
Pour la tester :
curl -XPOST -i -H "Content-Type: application/json" -d '{ "user": { "email":
"test@[Link]", "password": "12345678" } }'
[Link]
Réponse :
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 0
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
Vary: Accept, Origin
Authorization: Bearer
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyMDQiLCJzY3AiOiJ1c2VyIiwiYXVkIjpudWxsLCJpYXQi
OjE2NDYyMTk4MTEsImV4cCI6MTY0NjIyMzQxMSwianRpIjoiZWMxNDk3NWItOTNkYS00YTE1LTg1Y
[Link]-Ahh4To9qdheeMewFHmbZtvWa_gSYx5mD38
Set-Cookie:
_interslice_session=vOm61TiX5r758FI7DXxo07gRo%2F1lB08%2BrjKnf5N2q5oIOA4P3CI94
3u%2FbLSS3lJCyu%2FrFmLF8%2FliLCxhQTZN4DqNGgGgjZh6koGGyCxdFwshloUmSByg0D8vRA21
kEQcCguvQ8BwJ1alzn6N9fAjXussdx63iL87TSUGhuWgSv3Ze4BkD1WsRG%2FFlH%2BJ%2Ba4mraP
kGZCiQmfBlRLDjZ7n4mmWaE1ASsAhXmhf%2BeC79ag%2BQgE3ZOHkTzRUmnQft4BGeVC51ITCfvW4
7Cbi8elBQsfs2IzROxe9qtDOklzDcA%3D%3D--U%2FLRbl1%2FWXHqxKhR--
lcsdl17IGM7jOT14NN8qZg%3D%3D; path=/; HttpOnly; SameSite=Lax
ETag: W/"3f408df0bede3cd5797e2190eefd79d9"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: f1e51158-e4c6-42f2-bb94-535869cdccb5
X-Runtime: 0.256978
Server-Timing: start_processing.action_controller;dur=0.2275390625,
sql.active_record;dur=1.86376953125,
instantiation.active_record;dur=0.0888671875,
process_action.action_controller;dur=234.275390625
Transfer-Encoding: chunked
{"message":"You are logged in.","user":
{"id":204,"email":"test@[Link]","created_at":"2022-03-
01T19:50:54.482Z","updated_at":"2022-03-01T19:50:54.482Z"}}
Login with token
GET /member-data
Authentification nécessaire
Pour la tester :
curl -XGET -H [le token qui était dans Authorization dans la requête de
login] -H "Content-Type: application/json" [Link]
Réponse :
{"message":"If you see this, you're in!","user":
{"id":204,"email":"test@[Link]","created_at":"2022-03-
01T19:50:54.482Z","updated_at":"2022-03-01T19:50:54.482Z"}}
Logout
DELETE /users/sign_out
Authentification nécessaire
Pour la tester :
curl -XDELETE -H "Authorization: [le token qui était dans Authorization dans
la requête juste avant]" -H "Content-Type: application/json"
[Link]
Réponse :
{"message":"You are logged out."}