Skip to content

Instantly share code, notes, and snippets.

@bosskovic
Last active September 21, 2022 14:24
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save bosskovic/816623404b60b30726a8 to your computer and use it in GitHub Desktop.
Save bosskovic/816623404b60b30726a8 to your computer and use it in GitHub Desktop.
Devise as authentication solution for rails API
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
acts_as_token_authentication_handler_for User, fallback_to_devise: false
respond_to :json
protect_from_forgery with: :null_session
end
# spec/support/auth_helpers.rb
module AuthHelpers
def auth_token_to_headers(user)
request.headers['X-User-Email'] = "#{user.email}"
request.headers['X-User-Token'] = "#{user.authentication_token}"
end
def clear_auth_token
request.headers['X-User-Email'] = nil
request.headers['X-User-Token'] = nil
end
end
# app/controllers/v1/custom_devise/confirmations_controller.rb
module V1
module CustomDevise
class ConfirmationsController < Devise::ConfirmationsController
respond_to :json
# GET /resource/confirmation?confirmation_token=abcdef
def show
self.resource = User.confirm_by_token(params[:confirmation_token])
yield resource if block_given?
if resource.errors.empty?
render file: 'v1/custom_devise/sessions/create', locals: { current_user: resource}
else
render file: "#{Rails.root}/public/422.json", status: :unprocessable_entity, locals: { errors: resource.errors.full_messages }
end
end
end
end
end
# config/environment/development.rb
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
config.mailer_sender = '...'
config.skip_session_storage = [:http_auth, :token_auth]
# if :confirmable
config.allow_unconfirmed_access_for = 2.days
gem 'devise', '3.2.4'
gem 'simple_token_authentication', '1.5.0'
rails generate devise:install
rails generate devise User
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table(:users) do |t|
## Database authenticatable
t.string :email, null: false
t.string :encrypted_password
t.string :first_name, null: false
t.string :last_name, null: false
t.string :uuid
t.string :authentication_token
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Trackable
t.integer :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string :current_sign_in_ip
t.string :last_sign_in_ip
## Confirmable
t.string :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string :unconfirmed_email # Only if using reconfirmable
t.timestamps
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
add_index :users, :authentication_token, unique: true
add_index :users, :confirmation_token, unique: true
end
end
# app/controllers/v1/custom_devise/registrations_controller.rb
module V1
module CustomDevise
class RegistrationsController < Devise::RegistrationsController
respond_to :json
acts_as_token_authentication_handler_for User
skip_before_filter :authenticate_entity_from_token!, only: [:create]
skip_before_filter :authenticate_entity!, only: [:create]
skip_before_filter :authenticate_scope!
append_before_filter :authenticate_scope!, only: [:destroy]
# POST /users
def create
build_resource(param_keys_to_snake_case(sign_up_params))
if resource.save
sign_up(resource_name, resource)
render file: 'v1/custom_devise/registrations/create', status: :created
else
clean_up_passwords resource
render file: "#{Rails.root}/public/422.json", status: :unprocessable_entity, locals: { errors: resource.errors.full_messages }
end
end
# DELETE /users/UUID
def destroy
resource.deactivated_at = DateTime.now
resource.save!
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
end
private
def sign_up_params
params.fetch(:user).permit([:password, :passwordConfirmation, :email, :firstName, :lastName])
end
end
end
end
# config/routes.rb
Rails.application.routes.draw do
scope module: :v1, constraints: ApiConstraints.new(version: 1, default: :true, domain: APPLICATION_DOMAIN), defaults: {format: 'json'} do
devise_for :users, controllers: {
registrations: 'v1/custom_devise/registrations',
confirmations: 'v1/custom_devise/confirmations',
sessions: 'v1/custom_devise/sessions'
}
end
end
user = User.find_by_email(params[:user_email].presence)
if user && Devise.secure_compare(user.authentication_token, params[:user_token])
sign_in user, store: false
end
# app/controllers/v1/custom_devise/sessions_controller.rb
module V1
module CustomDevise
class SessionsController < Devise::SessionsController
respond_to :json
acts_as_token_authentication_handler_for User, fallback_to_devise: false
skip_before_filter :authenticate_entity_from_token!, only: [:create]
skip_before_filter :authenticate_entity!, only: [:create]
# POST /users/sign_in
def create
allow_params_authentication!
self.resource = warden.authenticate!(auth_options)
reset_token resource
render file: 'v1/custom_devise/sessions/create'
end
# DELETE /users/sign_out
def destroy
warden.authenticate!
reset_token current_user
end
private
def sign_in_params
params.fetch(:user).permit([:password, :email])
end
def reset_token(resource)
resource.authentication_token = nil
resource.save!
end
end
end
end
def generate_authentication_token
loop do
token = Devise.friendly_token
break token unless self.class.exists?(authentication_token: token)
end
end
# models/user.rb
class User < ActiveRecord::Base
acts_as_token_authenticatable
devise :database_authenticatable, :registerable, :recoverable, :trackable, :validatable, :confirmable
validates :first_name, presence: true
validates :last_name, presence: true
# left up to Devise to validate
# validates_presence_of :password_confirmation
# validates_uniqueness_of :email, case_sensitive: true
end
@aleksb86
Copy link

Excellent explanation! Its so helpful to understand Devise

@bosskovic
Copy link
Author

@aleksb86 I'm glad it could be of use to you even though it is quite aged by now. Here's an equally old blog post for which I created those gists:

http://perpetuum-mobile.net/tech/devise-as-authentication-solution-for-rails-api/

@imgyf
Copy link

imgyf commented May 27, 2020

Hi @bosskovic, do you have a strategy for users to reset password using rails API? Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment