radin reth
Posted on May 3, 2022
I am currently working on developing and api using grape and devise jwt for user user authentication.
Configure devise jwt is pretty straightforward all you need to do is just follow the instruction in the readme.
with Grape
Install gems
gem 'grape'
gem 'devise'
gem 'devise-jwt'
install the gems
bundle install
in app/api/api.rb
class Api < Grape::API
helpers AuthHelpers
helpers do
def unauthorized_error!
error!('Unauthorized', 401)
end
end
mount V1::UserRegistrationApi
end
in app/api/auth_helpers.rb
module AuthHelpers
def current_user
decoder = Warden::JWTAuth::UserDecoder.new
decoder.call(token, :user, nil)
rescue
unauthorized_error!
end
def token
auth = headers['Authorization'].to_s
auth.split.last
end
end
Currently, there 3 revocation strategies that devise jwt provide such as jtimatcher
, denylist
and allowlist
. but for now I am using allowlist
strategy.
in app/models/user.rb
class User < ApplicationRecord
include Devise::JWT::RevocationStrategies::Allowlist
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:jwt_authenticatable, jwt_revocation_strategy: self
end
Next, we need to generate AllowlistedJwt model
bin/rails g model AllowlistedJwt
Edit the migration file base on your need
class CreateAllowlistedJwts < ActiveRecord::Migration[6.1]
def change
create_table :allowlisted_jwts do |t|
t.string :jti, null: false
t.string :aud
t.datetime :exp, null: false
t.references :user, foreign_key: { on_delete: :cascade }, null: false
t.timestamps
end
add_index :allowlisted_jwts, :jti, unique: true
end
end
you will need to provide jwt secret. you can generate your own secret key using rails then store it somewhere that you can access to it
bin/rails secret
in config/initializers/devise.rb
Devise.setup do |config|
# other code
config.jwt do |jwt|
jwt.secret = Rails.application.credentials.devise_jwt_secret_key!
jwt.expiration_time = 3600
end
end
in app/api/v1/user_registration_api.rb
module V1
class UserRegistrationApi < Grape::API
namespace :user do
namespace :register do
before do
@user_mobile_number = UserRegistrationWithMobileNumberService.new params[:mobile_number]
end
# set response headers
after do
header 'Authorization', @user_mobile_number.token
end
post do
@user_mobile_number.register
end
end
put :verify do
current_user.verify(params[:code])
end
end
end
end
in app/services/user_registration_with_mobile_number_service.rb
class UserRegistrationWithMobileNumberService
attr_reader :mobile_number, :token
def initialize(mobile_number)
@mobile_number = mobile_number
end
def register
user = User.find_or_initialize_by mobile_number: mobile_number
if user.save
@token, payload = Warden::JWTAuth::UserEncoder.new.call(user, :user, nil)
user.on_jwt_dispatch(@token, payload)
# TODO: UserRegistrationJob.perform_later(user.id)
end
user
end
end
in spec/api/v1/user_registration_api_spec.rb
RSpec.describe V1::UserRegistrationApi, '/api/v1/user/register' do
let(:mobile_number) { '01234567' }
context 'with phone number' do
it 'creates new user' do
expect do
post '/api/v1/user/register', params: { mobile_number: mobile_number }
end.to change(User, :count).by 1
end
it 'responses the new created user' do
post '/api/v1/user/register', params: { mobile_number: mobile_number }
expect(json_body).to include mobile_number: mobile_number
expect(json_body).to include status: 'pending'
expect(json_body[:code]).to be_present
end
it 'responses with jwt authorization token' do
post '/api/v1/user/register', params: { mobile_number: mobile_number }
expect(response.headers['Authorization']).to match /(^[\w-]*\.[\w-]*\.[\w-]*$)/
end
context 'when mobile number is already registered' do
let!(:user) { create(:user, mobile_number: mobile_number)}
it 'responses with jwt token' do
post '/api/v1/user/register', params: { mobile_number: mobile_number }
expect(jwt_token).to match /(^[\w-]*\.[\w-]*\.[\w-]*$)/
end
end
end
context 'when confirm' do
before do
post '/api/v1/user/register', params: { mobile_number: mobile_number }
end
context 'with correct code' do
it 'changes status from pending to confirmed' do
put '/api/v1/user/verify', params: { code: response.body['code'] }, headers: { 'Authorization': "Bearer #{jwt_token}" }
expect(json_body(reload: true)[:status]).to eq 'confirmed'
end
end
context 'with wrong code' do
it 'unable to confirm' do
put '/api/v1/user/verify', params: { code: 'wrong-code' }, headers: { 'Authorization': "Bearer #{jwt_token}" }
expect(json_body(reload: true)[:status]).to eq 'pending'
end
end
context 'without authorized jwt token header' do
it 'responses unauthorized' do
put '/api/v1/user/verify', params: { code: json_body[:code] }
expect(response).to be_unauthorized
end
end
end
end
Additional notes:
Try it out in the console
newuser = UserRegistrationWithMobileNumberService.new('+85593555115')
registered_user = newuser.register
jwt = registered_user.allowlisted_jwts.last
payload = { jti: jwt.jti, aud: jwt.aud }
User.jwt_revoked? payload, registered_user
User.revoke_jwt payload, registered_user
Posted on May 3, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.