How to Sign a Token on LiveView
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)
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)
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
I assume, in retrospect, that it's decoding that with a something that's signed with PowInvitation.Plug on the way back in.
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?
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
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.
Good catch, updated the example!
@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?
I'm using your implementation instead. Looks better to me.
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
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.
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!