rails new my_api --api
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)
C’est parti pour quelques modifications dans le fichier config/initializers/cors.rb
Ces changements permettent d’autoriser n’importe quel site à faire des requêtes à l’API, pour autoriser une seule origine ➡️ origins "[url]"
# config/initializers/cors.rb
# 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: https://github.com/cyu/rack-cors
Rails.application.config.middleware.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
rails g devise:install
rails g devise User
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|
t.string :jti, null: false
t.datetime :exp, null: false
t.timestamps
end
add_index :jwt_denylist, :jti
end
end
:jwt_authenticatable
permet de dire à Devise queUser
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èleJwtDenylist
pour ça
# app/models/user.rb
class User < ApplicationRecord
# Il faut ajouter les deux modules commençant par jwt
devise :database_authenticatable, :registerable,
:jwt_authenticatable,
jwt_revocation_strategy: JwtDenylist
end
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
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 = JWT.decode(request.headers['Authorization'].split(' ')[1],
Rails.application.credentials.devise[:jwt_secret_key]).first
user_id = jwt_payload['sub']
User.find(user_id.to_s)
end
end
Deux nouveaux controllers à créer, qui modifieront les controllers de registration et de session de Devise
⚠️ Il faut créer ces fichiers dans un dossierusers
dansapp/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 resource.persisted?
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.setup do |config|
# Plein de code
config.jwt do |jwt|
jwt.secret = Rails.application.credentials.devise[:jwt_secret_key]
end
# Encore tout plein de code
end
-
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
# config/routes.rb
Rails.application.routes.draw 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 https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
# root "articles#index"
end
- Config pour utiliser les cookies dans
config/application.rb
# config/application.rb
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)
config.middleware.use ActionDispatch::Cookies
config.middleware.use config.session_store, config.session_options
# Plein de code
end
end
Et voilà ! 🎉
POST /users
Données attendues :
{
"user": {
"email": string,
"password": string
}
}
Pour la tester :
curl -XPOST -H "Content-Type: application/json" -d '{ "user": { "email": "test@example.com", "password": "12345678" } }' http://localhost:3000/users
Réponse :
=> {"message":"Signed up successfully.","user":{"id":[id],"email":"test@example.com","created_at":[timestamp],"updated_at":[timestamp]}
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@example.com", "password": "12345678" } }' http://localhost:3000/users/sign_in
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.eyJzdWIiOiIyMDQiLCJzY3AiOiJ1c2VyIiwiYXVkIjpudWxsLCJpYXQiOjE2NDYyMTk4MTEsImV4cCI6MTY0NjIyMzQxMSwianRpIjoiZWMxNDk3NWItOTNkYS00YTE1LTg1YTQtZmQ0ODllOTI2MTIwIn0.ZxRTdqSQ-Ahh4To9qdheeMewFHmbZtvWa_gSYx5mD38
Set-Cookie: _interslice_session=vOm61TiX5r758FI7DXxo07gRo%2F1lB08%2BrjKnf5N2q5oIOA4P3CI943u%2FbLSS3lJCyu%2FrFmLF8%2FliLCxhQTZN4DqNGgGgjZh6koGGyCxdFwshloUmSByg0D8vRA21kEQcCguvQ8BwJ1alzn6N9fAjXussdx63iL87TSUGhuWgSv3Ze4BkD1WsRG%2FFlH%2BJ%2Ba4mraPkGZCiQmfBlRLDjZ7n4mmWaE1ASsAhXmhf%2BeC79ag%2BQgE3ZOHkTzRUmnQft4BGeVC51ITCfvW47Cbi8elBQsfs2IzROxe9qtDOklzDcA%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@example.com","created_at":"2022-03-01T19:50:54.482Z","updated_at":"2022-03-01T19:50:54.482Z"}}
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" http://localhost:3000/member-data
Réponse :
{"message":"If you see this, you're in!","user":{"id":204,"email":"test@example.com","created_at":"2022-03-01T19:50:54.482Z","updated_at":"2022-03-01T19:50:54.482Z"}}
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" http://localhost:3000/users/sign_out
Réponse :
{"message":"You are logged out."}