Passwordless Logins in Phoenix
Maybe you’ve seen the slick “Magic Link” login that Slack and other apps now support: here’s how you can support that flow in a Phoenix app.
Why Passwordless?
Passwordless logins with emails are typically as, or more, secure than password-based authentication with email resets enabled, and are sufficiently secure for most web apps. They make login extremely simple for the user, and you no longer have to worry about securely handling passwords.
If you are handling things like financial or medical data, you may want to consider a multi-factor authentication scheme.
The basic flow
- A user wants to login, and submits their email
[email protected]
- Your application sends a short-lived login link (~15 minutes) to
[email protected]
- The user clicks the link and is authenticated
The Code
You probably already have a User
model, complete with an email
that
you want to allow access. If not, generate one with:
mix phoenix.gen.model User users email
The Sans Password helpers make our implementation incredibly simple. Their README and demo app are extremely helpful; much of this section comes straight from those.
Add Sans Password as a dependency:
# mix.exs
def deps do
[{:sans_password, "~> 1.0.0-beta"}]
end
And add a basic Guardian
module:
# my_app/guardian.ex
defmodule MyApp.Guardian do
alias MyApp.Mailer
@impl
def deliver_magic_link(user, magic_token, _params) do
user
|> Mailer.magic_link_email(magic_token)
|> Mailer.deliver_later
end
end
Add a mailer implementation (I like using Bamboo
):
# my_app/mailer.ex
defmodule MyApp.Mailer do
use Bamboo.Mailer, otp_app: :my_app
alias MyAppWeb.Emails
def magic_link_email(user, magic_token) do
Emails.magic_link(user, magic_token)
end
end
Put your email contents together in your Emails
module:
# my_app_web/emails.ex
defmodule MyAppWeb.Emails do
import Bamboo.Email
@from "[email protected]"
def magic_link(user, magic_token) do
body = "Your magic link: https://myapp.com/magic?token=#{magic_token}"
new_email()
|> from(@from)
|> to(user.email)
|> subject("Your magic link ✨")
|> html_body(body)
|> text_body(body)
end
And that’s it!
Now, on your login page, when a user want to login, call
MyApp.Guardian.send_magic_link(user)
To send the magic link. On your /magic
route, then exchange the short-lived
magic token for a longer-lived access token with
case MyApp.Guardian.exchange_magic(magic) do
{:ok, token, _} -> {:ok, %{token: token}}
error -> error
end