I am trying to authenticate my users in a mobile app via email-based Magic Link provided by Ash. The idea is to use Universal Links/AppLinks. The user taps their email like in a web version of the magic link, he receives an email, click on va magic link and with Universal/App links, the application is opened instead of the browser. The mobile application receive the “magic link token”, and then I can ask for an exchange with a JWT token. I also would like to keep the magic link behavior for the web part and creating the user if he does not exist.
What I have done:
- I generated a fresh Ash project with MagicLink authentication (no password strategy)
- Universal/App links work
- I created a new endpoint under the
api
pipeline to disable the CSRF for that endpoint and request a magic login link: /mobile/auth/magic-link/request. It works, but I am not sure I do it the Ash way. - Same for login with mobile magic link.
Here is my controller:
def request_mobile_magic_link(conn, %{"email" => email}) when is_binary(email) do input = User |> Ash.ActionInput.for_action(:request_magic_link, %{email: email}) |> Ash.ActionInput.set_context(%{private: %{ash_authentication?: true}}) case Ash.run_action(input) do :ok -> send_resp(conn, :ok, "") {:error, error} -> Logger.warning("request_magic_link error: #{Exception.message(error)}") send_resp(conn, :ok, "") end end def sign_in_with_mobile_magic_link(conn, %{"token" => _token} = params) do User |> Ash.Changeset.for_create(:sign_in_with_magic_link, params) |> Ash.Changeset.set_context(%{private: %{ash_authentication?: true}}) |> Ash.create() |> case do {:ok, record} -> {:ok, token, _claims} = AshAuthentication.Jwt.token_for_user(record, %{}) conn |> put_resp_header("content-type", "application/json") |> send_resp(:ok, JSON.encode!(%{token: token})) {:error, error} -> {:error, AshAuthentication.Errors.AuthenticationFailed.exception( strategy: AshAuthentication.Strategy.MagicLink, caused_by: error )} end end
I add to set the context with %{private: %{ash_authentication?: true}}
in both methods otherwise I received a forbidden error due to the forbid_if always()
policy, except for the bypass ash_authentication/lib/ash_authentication/checks/ash_authentication_interaction.ex at main · team-alembic/ash_authentication · GitHub
I created another endpoint to login from mobile, as I want to keep the magic login link from web, and I want to return a JWT token on success.
Should I create another bypass, or is it fine to set a private key?
I need two different emails depending if the user is requesting a magic link from the mobile app or from the web app. I would like to pass a mobile?: true
option to the third argument of the send
method but I do not find how to achieve that? I tried to add it to the context.
Here is the action on the User resource (generated by Ash):
create :sign_in_with_magic_link do description "Sign in or register a user with magic link." argument :token, :string do description "The token from the magic link that was sent to the user" allow_nil? false end upsert? true upsert_identity :unique_email upsert_fields [:email] # Uses the information from the token to create or sign in the user change AshAuthentication.Strategy.MagicLink.SignInChange metadata :token, :string do allow_nil? false end end
I do not understand the purpose of “metadata :token, :string …” and I didn’t find doc about this. Could someone explain me or point me to a doc/blog post?
It also seems that the AshAuthentication.Strategy.MagicLink.SignInChange
, put a JWT token under the metadata of the resource but I am not able to retrieve it with resource.__meta__.token
. This is the reason why I generated one in the controller.
Finally, I would like to know if this is the correct Ash way to do things or if I miss something more obvious.
I’m new to Ash, so I admit I’m still struggling with some concepts. Feel free to redirect me to the right documentation resource.