pow icon indicating copy to clipboard operation
pow copied to clipboard

How to Sign a Token on LiveView

Open johns10 opened this issue 4 years ago • 12 comments

Howdy, I'm trying to get PowInvitation working on LiveView. I'm on the home stretch. I've got everything working except for accepting the final invitation. The error I get is "Invitation doesn't exist." From reading other issues I deduced that it's because I'm not signing the token properly. This rip isn't working either, but I think I'm close. I think I'm sourcing my signing salt from the wrong place:

signing_salt = Atom.to_string(__MODULE__)
secret_key_base = UserDocsWeb.Endpoint.config(:secret_key_base)
secret = Plug.Crypto.KeyGenerator.generate(secret_key_base, signing_salt)
signed_token = Plug.Crypto.MessageVerifier.sign(user.invitation_token, signing_salt)

johns10 avatar Aug 12 '21 00:08 johns10

This also doesn't work:

        signing_salt = UserDocsWeb.Endpoint.config(:live_view)[:signing_salt]
        secret_key_base = UserDocsWeb.Endpoint.config(:secret_key_base)
        secret = Plug.Crypto.KeyGenerator.generate(secret_key_base, signing_salt)
        signed_token = Plug.Crypto.MessageVerifier.sign(user.invitation_token, signing_salt)

johns10 avatar Aug 12 '21 00:08 johns10

Hot dizzle dang! A new error!

        signing_salt = Atom.to_string(PowInvitation.Plug)
        secret_key_base = UserDocsWeb.Endpoint.config(:secret_key_base)
        secret = Plug.Crypto.KeyGenerator.generate(secret_key_base, signing_salt)
        signed_token = Plug.Crypto.MessageVerifier.sign(user.invitation_token, secret)
        url = Routes.pow_invitation_invitation_path(socket, :edit, signed_token)

function UserDocsWeb.PowInvitation.InvitationView.render/2 is undefined

johns10 avatar Aug 12 '21 01:08 johns10

I assume, in retrospect, that it's decoding that with a something that's signed with PowInvitation.Plug on the way back in.

johns10 avatar Aug 12 '21 01:08 johns10

You got it right, but I would just do this:


signed_token =
  %Plug.Conn{secret_key_base: UserDocsWeb.Endpoint.config(:secret_key_base)}
  |> Pow.Plug.put_config(otp_app: :my_app)
  |> PowInvitation.Plug.sign_invitation_token(user)

I've thought of letting these functions accept a module instead, so you could do this:

signed_token = PowInvitation.Plug.sign_invitation_token(UserDocsWeb.Endpoint, user)

Phoenix uses this extensively, and it would make it much easier to use Pow in cases where you don't have a Plug.Conn struct.

function UserDocsWeb.PowInvitation.InvitationView.render/2 is undefined

This would mean that you don't have a UserDocsWeb.PowInvitation.InvitationView module generated?

danschultzer avatar Aug 12 '21 14:08 danschultzer

Yes, that last error was trivial for me to figure out. I've got the whole thing up and running. I'll try it with your suggestion

johns10 avatar Aug 12 '21 21:08 johns10

Small correction, it was sign_invitation_token.

Now I get (Pow.Config.ConfigError) Pow configuration not found in connection. Please use a Pow plug that puts the Pow configuration in the plug connection.

I assume I just need to fetch the pow config and place it on this artificial Conn.

johns10 avatar Aug 12 '21 21:08 johns10

Good catch, updated the example!

danschultzer avatar Aug 12 '21 22:08 danschultzer

@danschultzer I solved it this way:

signed_token =
  %Plug.Conn{secret_key_base: UserDocsWeb.Endpoint.config(:secret_key_base)}
  |> Plug.Conn.put_private(:pow_config, Application.get_env(:userdocs_web, :pow))
  |> PowInvitation.Plug.sign_invitation_token(user)

Is the eventual play to just put all the stuff you need on the socket, and access socket.assigns.conn?

johns10 avatar Aug 13 '21 02:08 johns10

I'm using your implementation instead. Looks better to me.

johns10 avatar Aug 13 '21 02:08 johns10

Here's my PowInvitation implementation in LiveView. Omitting front end, and some implementation specific stuff:

User clicks "Send Invitation" on the form, it hits the send invitation envent:

  def handle_event("send-invitation", _params, socket = %{assigns: %{changeset: %{params: params}}}) do
    user_attrs = Map.get(params, "user")

    case Users.invite_user(%Users.User{}, user_attrs) do
      {:ok, user} ->
        signed_token =
          %Plug.Conn{secret_key_base: UserDocsWeb.Endpoint.config(:secret_key_base)}
          |> Pow.Plug.put_config(otp_app: :userdocs_web)
          |> PowInvitation.Plug.sign_invitation_token(user)

        %{
          url: Routes.pow_invitation_invitation_path(socket, :edit, signed_token),
          module: UserDocsWeb.PowInvitation.MailerView,
          user: user,
          invited_by: socket.assigns.current_user,
        }
        |>  Users.send_email_invitation()

        team = Users.get_team!(socket.assigns.team.id, %{preloads: preloads})
        changeset = Users.change_team(team, socket.assigns.changeset.params)

        {:noreply, socket |> assign(:team, team) |> assign(:changeset, changeset)}
      {:error, %{changes: %{email: email}, errors: [email: {"has already been taken", _}]}} ->
        # Handle better
        {:noreply, socket}
      {:error, changeset} ->
        {:noreply, socket}
    end

Domain Functions:

  def invite_user(%User{} = user, attrs \\ %{}) do
    User.invite_changeset(user, attrs)
    |> UserDocs.Repo.insert()
  end

  def send_email_invitation(attrs) do
    attrs
    |> Email.cast_onboarding()
    |> Email.onboarding()
    |> Email.send()
  end

johns10 avatar Aug 13 '21 02:08 johns10

Basically, it signs the token, hydrates the email on the form, then sends the email + db stuff to the backend. I probably could have put PowInvitation in there, but it's not super transparent to me without using the vanilla controllers.

johns10 avatar Aug 13 '21 03:08 johns10

Is the eventual play to just put all the stuff you need on the socket, and access socket.assigns.conn?

I believe all the plug functions should accept a module atom along with conn to make this easier. This is how Phoenix does it. That way you can just do:

PowInvitation.Plug.sign_invitation_token(MyAppWeb.Endpoint, user)

This would probably be the cleanest approach.

Thanks for providing the working code!

danschultzer avatar Sep 27 '21 15:09 danschultzer