pow-auth / pow_assent

Multi-provider authentication for your Pow enabled app
https://powauth.com
MIT License
318 stars 50 forks source link

[Advice] Need to add an attribute to new users from session #244

Open coladarci opened 10 months ago

coladarci commented 10 months ago

We have had a need surface where, when a new user is created via Pow Assent, I have to somehow hook into the creation process to pull something from the session and add to the user attributes.

Given the large number of "hooks" you have in place, there's a good chance this is possible but I can't seem to find any that would do what I need. Am I missing something? Or another clever solution?

Thanks for any advice!

danschultzer commented 9 months ago

FWIW there's a related question to this in Pow where a plug is sufficient to append params: https://github.com/pow-auth/pow/issues/702#issuecomment-1628149650

For PowAssent it's different since we pull the user params from the provider response. There's no easy way of injecting it as the callback is called and then the response is processed all the way to creating the user:

https://github.com/pow-auth/pow_assent/blob/7311193d708a0e3696373cc42c6809fe552b7929/lib/pow_assent/plug.ex#L95-L109

First approach will be to inject it in the schema changeset or context, or higher level setting up a custom strategy where you modify the callback response. But neither gives access to the conn.

A more clever way would be to use process dictionary to hack it:

defmodule MyAppWeb.PowAssent.InjectRegistrationPlug do
  @moduledoc """
  This plug injects attributes during user registration.

  ## Example

      plug MyAppWeb.Pow.InjectRegistrationPlug
  """

  def init(opts), do: opts

  def call(%{private: %{phoenix_controller: PowAssent.Phoenix.AuthorizationController, phoenix_action: :callback}} = conn, _opts) do
    put_pow_assent_user_params(conn)

    conn
  end

  def call(%{private: %{phoenix_controller: PowAssent.Phoenix.RegistrationController, phoenix_action: :create}} = conn, _opts) do
    put_pow_assent_user_params(conn)

    conn
  end

  def call(conn, _opts), do: conn

  defp put_pow_assent_user_params(conn) do
    # ...

    Process.put(:pow_assent_user_params, params)
  end
end
defmodule MyApp.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema
  use PowAssent.Ecto.Schema

  # ...

  def changeset(user_or_changeset, attrs) do
    user_or_changeset
    |> pow_changeset(attrs)
    |> inject_attributes()
  end

  defp inject_attributes(changeset) do
    case Process.get(:pow_assent_user_params) do
      nil -> changeset
      attributes -> # ...
    end
  end
end

I'll think about how to make it simple to hook into params transformation though. The above should work, and maybe is a reasonable approach, but it would be nice to have full control over the transformation where it actually happens.

coladarci commented 9 months ago

The only solution I came up with is

  1. DB Sets a temporary garbage value on create
  2. We redirect to a custom route on reg
  3. Process the session THERE and update record
  4. Redirect to wherever we normally would have redirected to Not awful but would love it to be less hacky if there's a way
danschultzer commented 9 months ago

Yeah, I think I'll add a way to be able to transform the conn right after provider callback has been processed. You can already set callbacks for when a new session is created (you can set them with PowAssent.Plug.put_create_session_callback/2), which is used do for the reauthorization plug. I think something similar should exist so you can transform the conn (with user params assign) during provider callback. This should make it possible to do anything with the user params.