Reputation: 341
Say I have a simple User
model:
class User < ApplicationRecord
devise :database_authenticatable, :registerable, :recoverable, :rememberable,
:trackable, :validatable
belongs_to :company
end
As well as a model for companies:
class Company < ApplicationRecord
has_many :users
validates :admin, :name, presence: true
delegate :email, to: :admin
def email=(email)
self.admin = User.find_by(email: email) ||
User.new(email: email)
end
end
With routes like:
Rails.application.routes.draw do
# API
namespace :api, defaults: { format: "json" } do
namespace :v1 do
# Companies
resources :companies
# Users
resources :users, except: :destroy
devise_scope :user do
post "/users/sign_in", to: "sessions#create"
end
end
end
end
With a SessionsController
that looks like:
module Api
module V1
class SessionsController < Devise::SessionsController
rescue_from ActionController::ParameterMissing, with: :invalid_login
skip_before_action :verify_authenticity_token, only: :create
protect_from_forgery with: :null_session
respond_to :json
def create
@user = Forms::SigninForm.new(signin_params)
if @user.save
@user = @user.user
render "api/v1/users/show"
else
invalid_login(@user.errors)
end
end
def destroy
sign_out(resource_name)
end
def invalid_login(messages)
render json: { errors: messages }
end
private
def signin_params
user_params.merge(auth_options: auth_options)
end
def user_params
params.require(:user)
.permit(:email, :password)
.merge(warden: warden)
end
end
end
end
Where the SigninForm
is defined like:
module Forms
class SigninForm < BaseFormObject
validate :user_exists
attr_reader :email, :password, :warden
def save
return false if invalid?
ActiveRecord::Base.transaction do
user.auth_tokens.create(portal: portal_request?)
unless portal_request?
user
.auth_tokens
.second_to_last&.
destroy
end
end
true
end
def user
@user ||=
begin
warden.authenticate(auth_options) ||
User.find_by(admin_password_query, email, password)
end
end
private
def admin_password_query
"email ilike ? and admin_password = ?".freeze
end
def user_exists
return if user.present?
errors.add(:user, "Invalid email or password.")
end
def portal_request?
@portal_request
end
def user_params
{
email: email,
password: password
}
end
def auth_options
{
scope: @scope,
recall: @recall
}
end
end
end
Is there a simple and straight forward way to delegate authentication for a Company
to it's admin
? In such a way that, when a company is created, the associated User
admin is the one who holds the auth token and password, and when the a user would like to sign in as a company - the password is checked against the user model, not the company.
Is there a way to, perhaps, use a custom devise strategy?
I hope my question is clear. Thanks for reading.
Upvotes: 1
Views: 766
Reputation: 341
To update, I came up with a solution that was simple enough, at least for me. During that time I learned about warden's use of the strategy pattern, which allowed me to refactor the SigninForm
by pulling out any of means of authentication seen in the form object (i.e: finding user by email and an "admin password") into actual warden strategies, registering them with warden's Manager
middleware, and then simply listing the authentication strategies used in the SigninForm
that we finally pass to warden's Proxy
class which runs these strategies until one succeeds (or they all fail).
So, we can define strategies like:
# in /config/initializers/auth_strategies.rb
require "devise/strategies/authenticatable"
module Devise
module Strategies
class AdminAuthenticatable < Authenticatable
def valid?
params[:user] && params[:email] || params[:password]
end
def authenticate!
pass unless params[:user]
user = User.find_by("email ilike ? and admin_password = ?",
params[:email],
params[:password])
success!(user) if user
fail!
end
end
class CompanyAuthenticatable < Authenticatable
def valid?
params[:company]
end
def authenticate!
user = User.find_by_email(params[:company][:email])
if user&.valid_password?(params[:company][:password])
if Company.find_by(admin_id: user.id)
success!(user)
else
fail!
end
else
fail!
end
end
end
end
end
Register them with the Manager
middleware:
# in /config/initializers/devise.rb
Devise.setup do |config|
config.warden do |manager|
manager.strategies.add(
:company_authenticatable,
Devise::Strategies::CompanyAuthenticatable
)
manager.strategies.add(
:admin_authenticatable,
Devise::Strategies::AdminAuthenticatable
)
end
end
Finally, refactoring the form object and simply passing in used strategies to warden there:
module Forms
class SigninForm < BaseFormObject
validate :user_exists
validate :user_administrates_company, if: :company_signin?
attr_reader :email, :password, :warden
def save
return false if invalid?
ActiveRecord::Base.transaction do
user.auth_tokens.create(portal: portal_request?)
unless portal_request?
user
.auth_tokens
.second_to_last&.
destroy
end
end
true
end
def user
@user ||=
begin
warden.authenticate(*strategies, auth_options)
end
end
def company
@company ||=
user && Company.find_by(admin_id: user&.id)
end
private
def user_exists
return if user.present?
errors.add(:user, "Invalid email or password.")
end
def user_administrates_company
return if company.present?
errors.add(:company, "Invalid email or password.")
end
def portal_request?
@portal_request
end
def company_signin?
@resource.eql?("company")
end
protected
def strategies
%i[database_authenticatable admin_authenticatable company_authenticatable]
end
def auth_options
{}
end
end
end
Upvotes: 0
Reputation: 2727
It is possible to add more authenticated models to devise. https://github.com/plataformatec/devise/wiki/How-to-Setup-Multiple-Devise-User-Models
You could than create a custom action, where you call sign_in(@company)
. Only allow this action for admins of the company. Thats it!
Upvotes: 1