This is a template of a Rails 7 API-only setup with Devise (confirmable) & Devise JWT (AllowList revocation strategy + AUD claim). Highly recommend reading articles on JWT revocation from Devise JWT.
Additionally, this API has a custom Devise User param (:name
) and namespacing-like routes (/v1/users/...
) for default Devise controllers. If you don't want this, browse the previous commit on main.
I'm using rbenv as my ruby env manager and rbenv-gemset extension to create collections of gems.
To set this up from scratch:
brew upgrade ruby-build # On MacOS to update the Ruby versions list
rbenv install 3.3.4
rbenv global 3.3.4
gem install rails
rails new my_app --api --database=postgresql --skip-test # Don't forget to add your preferred options
-
Uncomment
gem "rack-cors"
inGemfile
-
bundle install
-
Uncomment CORS config in
cors.rb
. For dev purposes, let any connection through:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "*"
resource "*",
headers: :any,
methods: [ :get, :post, :put, :patch, :delete, :options, :head ]
end
end
Setup Devise
-
bundle add devise
-
rails generate devise:install
-
Add the following line to
development.rb
:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
-
rails generate devise User
-
Uncomment the "Confirmable" section in the
devise_create_users.rb
migration. -
rails db:create db:migrate
-
Follow these:
-
Add
devise :confirmable
to theUser
model. -
Use letter_opener to open confirmation letters in development (follow the link for instructions).
Setup Devise JWT
-
bundle add devise-jwt
-
Generate a secret:
rails secret
-
EDITOR="code --wait" rails credentials:edit
:
devise_jwt_secret_key: <your_secret>
Note! Devise JWT devs encourage to use a secret different from the secret_key_base
.
- Add the config to
devise.rb
:
Devise.setup do |config|
# ...
config.jwt do |jwt|
jwt.secret = Rails.application.credentials.devise_jwt_secret_key!
end
end
- Create
allowlisted_jwts
table:
rails g migration create_allowlisted_jwts
:
def change
create_table :allowlisted_jwts do |t|
t.string :jti, null: false
t.string :aud, null: false
t.datetime :exp, null: false
t.references :user, foreign_key: { on_delete: :cascade }, null: false # user singular (!)
end
add_index :allowlisted_jwts, :jti, unique: true
end
rails db:migrate
- Create
AllowlistedJwt
model:
class AllowlistedJwt < ApplicationRecord
end
- Include JWT in
User
:
class User < ApplicationRecord
include Devise::JWT::RevocationStrategies::Allowlist
devise ...
:jwt_authenticatable, jwt_revocation_strategy: self
end
- Add the
aud_header
config todevise.rb
:
Devise.setup do |config|
# ...
config.jwt do |jwt|
...
jwt.aud_header = "JWT_AUD" # Change to the preferred one
end
end
This header needs to be in your request headers, or else there's going to be an error:
ActiveRecord::NotNullViolation (PG::NotNullViolation: ERROR: null value in column "aud" of relation "allowlisted_jwts" violates not-null constraint)
rails g migration add_name_to_users
:
def change
add_column :users, :name, :string, null: false # Use preferred options
end
rails db:migrate
- Add a validation to User if you choose to have a
null: false
param:
user.rb
class User < ApplicationRecord
...
validates_presence_of :name
end
- Configure Devise permitted params:
application_controller.rb
class ApplicationController < ActionController::API
...
before_action :configure_permitted_parameters, if: :devise_controller?
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [ :name ])
devise_parameter_sanitizer.permit(:account_update, keys: [ :name ])
end
end
routes.rb
# Before: devise_for :users, defaults: { format: :json }
devise_for :users, defaults: { format: :json }, path: "v1/users"
This way, the routes change in a namespacing-like manner but with no hustle about the actual namespacing consequences, like, for example, changes in resourse_name
s or controller methods.
See, for example, an excerpt from the #devise_for
docs (:module
option):
Also pay attention that when you use a namespace it will affect all the helpers and methods for controllers and views. For example, using the above setup you'll end with following methods: current_publisher_account, authenticate_publisher_account!, publisher_account_signed_in, etc.
rails s
Don't forget the JWT_AUD
header. I set it to postman
.
Before running this, click the confirmation link in the letter_opener
letter in your browser.
Copy the Authorization header value ("Bearer ...") and paste with the next request.
You're welcome to file an issue or share some thoughts in discussions.