allow for multi-tenancy
first of all, compliments to this nice and clean standalone implementation for passwordless authentication
in my use case we have a multi-tenant application where a user is not purely identified by it's email address but where there is another property that identifies the so called 'tennant'. this could be for example the hostname (in multi homed setups), an explicit tenant identifier in the path, etc
i've looked at the codebase and the current implementation is limiting to a single parameter name.
to make the code more general, instead of passing in only the email identifier to the fetch_resource_for_passwordless method, it's more generic to pass in all params. like that the users can user all params without restrictions
def find_authenticatable
authenticatable_class.fetch_resource_for_passwordless(params)
end
in the readme i put some examples, the third shows how it could be used to allow for multi-tenancy based on an extra param.
def self.fetch_resource_for_passwordless(params)
find_by(email: params[:passwordless][:email])
# # auto-create users
# find_or_create_by(email: params[:passwordless][:email])
# # multi tenant example
# tenant = Tenant.find_by(tenant_uuid: params[:passwordless][:tenant_uuid])
# find_or_create_by(email: params[:passwordless][:email], tenant: tenant)
end
to be even more generic, the full request would need to be passed to be able to access the HOSTNAME for example in which case the host name is the discriminator of the tenant.
further i noticed also the following: the code assumes configuration of the name of the email field. However, one could simplify the code as follows. By default the code could assume the name of the field is 'email', and to cope for the case where the field name is different, one simply just needs to add an override for fetch_resource_for_passwordless. this simplification was also implemented in the fork. the sole impact is that the default template can not be rendered dynamically but that's almost a non issue because the user will override these anyhow.
def self.fetch_resource_for_passwordless(params)
find_by(email_address: params[:passwordless][:email])
end
https://github.com/koenhandekyn/passwordless?organization=koenhandekyn&organization=koenhandekyn
note : does anyone know of an omni-auth adaptor? that seems to make sense to me as an easy way to bridge to devise also for example
Hi @koenhandekyn! Thanks for the kind words 😊
I like your suggestions. Would definitely want to add something like that if you or someone made a PR. I'm afraid I don't have the time right now. Also, I would very much prefer if we can do it in a backwards compatible way. At least until 1.0.
I'd love this too! I may need to work around it by creating multiple user models, which could cause my app problems :(
I'm not against passing the whole params as long as we add checks and fallbacks to not make it a breaking change.
PRs welcome 😊
I created a first-pass version that seems to work locally allowing email and phone number passwordless by overriding some methods in the session controller and overriding the mailer. Def not the cleanest, but it seems to work.
Below is just a first-pass version; there are probably a lot of bugs
In a bit of a hurry but when I get some time I'd love to loop back and actually contribute!
Overriden controller methods in an initializer
# require_dependency 'passwordless/sessions_controller'
Rails.application.config.after_initialize do
Passwordless::SessionsController.class_eval do
def find_authenticatable
if params[:auth_method] == 'email'
email = params[:passwordless][:email].downcase.strip
elsif params[:auth_method] == 'phone'
email = params[:passwordless][:phone].downcase.strip
else
raise "Unknown auth method #{params[:auth_method]}"
end
# email = passwordless_session_params[email_field].downcase.strip
if authenticatable_class.respond_to?(:fetch_resource_for_passwordless)
authenticatable_class.fetch_resource_for_passwordless(params)
else
authenticatable_class.where("lower(#{email_field}) = ?", email).first
end
end
def passwordless_session_params
params.require(:passwordless).permit(:token, authenticatable_class.passwordless_email_field, :email_field, :phone_field)
end
end
end
Overriden mailer
module Passwordless
# The mailer responsible for sending Passwordless' mails.
class MultiTenantMailer < Passwordless.config.parent_mailer.constantize
default from: Passwordless.config.default_from_address
# Sends a token and a magic link
#
# @param session [Session] An instance of Passwordless::Session
# @param token [String] The token in plaintext. Falls back to `session.token` hoping it
# is still in memory (optional)
def sign_in(session, token = nil)
@token = token || session.token
@magic_link = Passwordless.context.url_for(
session,
action: "confirm",
id: session.to_param,
token: @token
)
puts "TEST MAILER"
# email_field = session.authenticatable.class.passwordless_email_field
mail(
to: session.authenticatable.send(:email),
subject: I18n.t("passwordless.mailer.sign_in.subject")
)
end
end
end
My model
class User < ApplicationRecord
passwordless_with :phone
def self.fetch_resource_for_passwordless(params)
if params[:passwordless][:phone].present?
User.find_by(phone: params[:passwordless][:phone])
elsif params[:passwordless][:email].present?
User.find_by(email: params[:passwordless][:email])
end
end