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

How to Sign a Token on LiveView #642

Open
johns10 opened this issue Aug 12, 2021 · 12 comments
Open

How to Sign a Token on LiveView #642

johns10 opened this issue Aug 12, 2021 · 12 comments

Comments

@johns10
Copy link

johns10 commented Aug 12, 2021

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
Copy link
Author

johns10 commented Aug 12, 2021

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
Copy link
Author

johns10 commented Aug 12, 2021

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
Copy link
Author

johns10 commented Aug 12, 2021

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

@danschultzer
Copy link
Collaborator

danschultzer commented Aug 12, 2021

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?

@johns10
Copy link
Author

johns10 commented Aug 12, 2021

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
Copy link
Author

johns10 commented Aug 12, 2021

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.

@danschultzer
Copy link
Collaborator

Good catch, updated the example!

@johns10
Copy link
Author

johns10 commented Aug 13, 2021

@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
Copy link
Author

johns10 commented Aug 13, 2021

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

@johns10
Copy link
Author

johns10 commented Aug 13, 2021

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
Copy link
Author

johns10 commented Aug 13, 2021

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.

@danschultzer
Copy link
Collaborator

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!

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

No branches or pull requests

2 participants