Subscribe to access all episodes. View plans →

#44: Passwordless Authentication with Veil

Published April 30, 2018

Elixir 1.6

Phoenix 1.3

Veil

View source on GitHub


We have a site that lists a few different Albums.

There’s a link to add an album to the the site and a link to edit a album.

Currently anyone can an album and change an attributes like the price.

Let’s add authentication so that a user needs to be signed in, in order to access these pages.

In episode 42 we covered a more tradition sign in/sign up with a username and password.

In this let’s take a different approach and implement “passwordless authentication”.

To do that we’ll use a relatively new library - Veil.

How will passwordless authentication work with Veil?

First a user goes to a sign on page where they input their email address.

Then a email is sent with a sign in link that contains a “request” token.

When the user opens the email and clicks the link they, are redirected back to our site.

Veil will then validate the request token and - if it’s validated - a session is created for our user, and our user is saved to the database.

Alright, let’s get started.

The first thing we’ll do is add Veil to our list of dependencies. Here I’m actually going to download Veil from GitHub since there are some recent changes that haven’t made their way to Hex yet.

mix.exs

...
defp deps do
  [
  ...
  {:veil, github: "zanderxyz/veil"},
  ...
  ]
end

Then we’ll download it.

$ mix deps.get

Then we’ll install veil

$ mix veil.add

And we can Veil adds some modules and a migration for us.

It then gives us some instructions to get it working.

Let’s take a look at a few of the modules that Veil gives us.

We’ll start with theuser.ex module.

And in our schema a user that has_many requests - this is the login request and sessions, which is created when a user clicks their email to sign in.

Let’s also take a look at the veil.ex context module.

This provides different function to get a user - by id, and email, create a user, verify a user, and send the login email.

Now let’s look at the routes Veil has added.

We’ll open our router.ex

In our browser pipeline we see that Veil has added a UserId plug - this verifies if a user is signed in.

Then let’s go to the default routes Veil added.

There’s one to create a user, the sign in page, sign in the user, and sign them out.

Now let’s open up our config.exs

Veil has added 3 configuration blocks. The first allows us to customize our sign in emails. Let’s update our site name with the name of our site: “Record Store”, our emails from name, and the email from address (jon@elixircasts.io)

There are also fields for how long our sign in link is valid - the default here is 1 hour, our session expiration, and session refresh interval.

Veil uses Quantum to schedule the cleanup of expired requests and sessions. By default we see that it’s set to run every day at midnight.

Finally, Veil uses Swoosh to send emails for authentication. Swoosh integrates with several different transactional email providers out-of-the-box, but let’s keep it simple and stick with Sendgrid for our example. You’ll want to update this to include your Sendgrid api key here.

config/config.exs

...
# -- Veil Configuration    Don't remove this line
config :veil,
  site_name: "Record Store",
  email_from_name: "Jon Landau",
  email_from_address: "jon@elixircasts.io",
  sign_in_link_expiry: 3_600,
  session_expiry: 86_400 * 30,
  refresh_expiry_interval: 86_400

config :veil,Veil.Scheduler,
  jobs: [
    # Runs every midnight to delete all expired requests and sessions
    {"@daily", {Teacher.Veil.Clean, :expired, []}}
  ]

config :veil, TeacherWeb.Veil.Mailer,
  adapter: Swoosh.Adapters.Sendgrid,
  api_key: "YOUR_API_KEY"

# -- End Veil Configuration

Now we can go to the command line and run our migration to create our different schemas, like our veil_users schema, that Veil adds.

$ mix ecto.migrate

And running our migration we get an error undefined function page_path/2 in our session_controller on line 20. I’ve customized the routes of our app and in doing so removed the page_path

But this is actually one of the really nice parts of Veil - since most of it is copied into our project it’s incredibly easy to customize it to work how we want. So let’s open our session_controller and swap our old route with our new route - our album path.

lib/teacher_web/controllers/veil/session_controller.ex

...
|> redirect(to: album_path(conn, :index))
...

With that we can go back to the command line and run our migration.

$ mix ecto.migrate

And perfect - it works.

Now let’s add a link users can click to get to our sign on page. Let’s open our app.html.eex template. And we’ll add a link to the new user path provided by Veil.

Template path: lib/teacher_web/templates/layout/app.html.eex

  ...
  <%= link "Sign in", to: user_path(@conn, :new) %>
  |
  <%= link "Home", to: album_path(@conn, :index) %>
  ...

If we then go to the browser and click our new link, we are directed to a “Sign In” page where we can enter our email.

Let’s go ahead an put in an email we want to use to sign in.

And if we submit it we see a confirmation page telling us to click the link in our email to sign in.

Checking our email - we see we did receive an email to sign in.

We see all the information we specified in our config, including our site_name, from name, and that our sign in link will expire after 1 hour.

Let’s click “Sign In” and we are taken back to our site. Let’s take a quick look at our development logs.

And it looks like everything was successful. A record was written to “veil_sessions”, our “veil_requests” record was removed, and our “veil_user” record was updated.

Everything looks good - we just need to update our app to recognize if a user is signed in - and if they are display the appropriate links.

Let’s go back to our router, and let’s take a closer look at the Veil UserId plug that being called for our browser requests.

This plug verifies our session and if it’s valid, assigns our user_id to the connection as veil_user_id.

Let’s use this to determine whether we want to display a sign in or sign out link.

We’ll open our app.html.eex template. And update it so that if our veil_user_id is present, we’ll display our sign out link, passing it the @session_unique_id that’s also added by the UserId plug.

If our user id is not present we’ll display our link to sign in.

Template path: lib/teacher_web/templates/layout/app.html.eex

    <%= if assigns[:veil_user_id] do %>
      <%= link "Sign out", to: session_path(@conn, :delete, @session_unique_id) %>
    <% else %>
      <%= link "Sign in", to: user_path(@conn, :new) %>
    <% end %>
    |
    <%= link "Home", to: album_path(@conn, :index) %>

Then if we go back to the browser because we’re signed in, we see our sign out link.

Let’s try it out.

Great - it worked - we’re signed out.

And if we check our database, our veil_sessions record was removed. Now let’s see how we can limit access to our create and edit pages to only users who are signed in.

Let’s take a look at another module Veil provides - the Authenticate plug.

The Authenticate plug - as we can see from it’s moduledoc - restricts access to logged in users.

It checks if the veil_user_id has been set on the connection. If it is the connection is returned and allowed to proceed. Otherwise, it will redirect us to our sign in page.

This is just what we need to restrict access to different routes.

But first, let’s update the redirect to include a flash message letting our users know they need to be signed in.

lib/teacher_web/plugs/veil/authenticate.ex

...
def call(conn, _opts) do
  unless is_nil(conn.assigns[:veil_user_id]) do
    conn
  else
    conn
      |> Phoenix.Controller.put_flash(:error, "You need to sign in to access that page")
      |> Phoenix.Controller.redirect(to: TeacherWeb.Router.Helpers.user_path(conn, :new))
  end
end
...

Then let’s open our album_controller.ex and let’s use the Authenticate plug to restrict users that aren’t signed in to only be able to access the “index” and “show” actions

lib/teacher_web/controllers/album_controller.ex

...
plug TeacherWeb.Plugs.Veil.Authenticate when action not in [:show, :index]
...

Now let’s go back to the browser.

And if we try to access a page that requires authentication, we’re redirected to our “Sign in” page.

Let’s try signing in. I’ve clicked the sign in link off screen, but we can see the “Sign out” link is now displayed to show that we’re signed in.

And if we try a restricted page again - we can access it and we can update our albums.

Now let’s say we wanted to display more information about our signed in user. how could we access a full user struct in order to easily render different fields.

Specifically, let’s update our site so that when a user is signed in, we display their email next to the “Sign out” link.

We’ll open our router.ex and below the UserId plug in our browser pipeline let’s add another plug provided by Veil named User.

It will take the veil_user_id if it’s present, lookup our user from it, and then add it to the assigns as veil_user.

Let’s add it.

lib/teacher_web/router.ex

...

pipeline :browser do
  plug(:accepts, ["html"])
  ...
  plug(TeacherWeb.Plugs.Veil.UserId)
  plug(TeacherWeb.Plugs.Veil.User)
end

...

Then we’ll open our app.html.eex template.

And if a user is signed in, let’s display their email from our user.

Template path: lib/teacher_web/templates/layout/app.html.eex

  ...
  <%= if assigns[:veil_user_id] do %>
    <span>Signed in as <%= @veil_user.email %></span>
    <%= link "Sign out", to: session_path(@conn, :delete, @session_unique_id) %>
  <% else %>
    <%= link "Sign in", to: user_path(@conn, :new) %>
  <% end %>
  |
  <%= link "Home", to: album_path(@conn, :index) %>
  ...

We’ll go back to our browser - and we see our email is displayed.

And again, clicking “Sign out” removes our session and our email is no longer displayed.

© 2024 HEXMONSTER LLC