Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow for multi-tenancy #83

Open
koenhandekyn opened this issue May 7, 2020 · 4 comments
Open

allow for multi-tenancy #83

koenhandekyn opened this issue May 7, 2020 · 4 comments

Comments

@koenhandekyn
Copy link

koenhandekyn commented May 7, 2020

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

@mikker
Copy link
Owner

mikker commented May 10, 2020

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.

@evankozliner
Copy link

I'd love this too! I may need to work around it by creating multiple user models, which could cause my app problems :(

@mikker
Copy link
Owner

mikker commented Jan 24, 2024

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 😊

@evankozliner
Copy link

evankozliner commented Jan 27, 2024

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

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

No branches or pull requests

3 participants