Feb 4, 2019

Phoenix Authentication with Pow - Part 2

Last time we set up a basic installation of Pow. Today we’ll add on session persistence, registration confirmation and password reset functionality.

So let’s get at it!

Getting started

If you’ve been following along you can continue with the code from part 1, or you can grab the code from GitHub.

Clone the Repo:

If cloning:

Terminal
git clone -b part-01 https://github.com/riebeekn/phx-auth-with-pow warehouse
cd warehouse

Let’s create a branch for today’s work:

Terminal
git checkout -b part-02

And make sure we have our dependencies and database set-up.

Terminal
mix deps.get
Terminal
cd assets && npm install

Note: before running ecto.setup you may need to update the username / password database settings in dev.config to match your local postgres settings, i.e.

/config/dev.config …line 69
# Configure your database
config :warehouse, Warehouse.Repo,
  username: "postgres",
  password: "postgres",
  database: "warehouse_dev",
  hostname: "localhost",
  pool_size: 10
Terminal
cd ..
mix ecto.setup

With that out of the way… let’s see where we are starting from.

Terminal
mix phx.server

If we navigate to http://localhost:4000/ we’ll see our application.

If we create a user and login, we’ll see we have access to our products page and the navigation links update intelligently based on the fact that a user is logged in.

One thing you’ll notice is if we close and re-open our browser we lose our user session (i.e. we get logged out).

This isn’t always the behaviour we’d prefer, so let’s see how we can add session persistence.

Adding session persistence

Pow provides us with an easy way of including session persistence, the first step is to update config.exs.

/config/config.exs …line 20
# Pow configuration
config :warehouse, :pow,
  user: Warehouse.Users.User,
  repo: Warehouse.Repo,
  web_module: WarehouseWeb,
  extensions: [PowPersistentSession],
  controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks

We’ve added the PowPersistentSession extension as well as a controller_callback.

Next we need to update endpoint.ex, adding a new Plug for session persistence.

/lib/warehouse_web/endpoint.ex …line 45
# enable Pow session based authentication
plug Pow.Plug.Session, otp_app: :warehouse
# enable Pow persistent sessions
plug PowPersistentSession.Plug.Cookie

Finally we should update the login form to give the user the option of whether they want their session to be persisted or not. We’ll add a simple check-box.

/lib/warehouse_web/templates/pow/session/new.html.eex …line 18
<%= label f, :persistent_session, "Remember me" %>
<%= checkbox f, :persistent_session %>

<div>
  <%= submit "Sign in now!" %>
</div>

To see this all in action we need to restart the server.

Terminal
mix phx.server

Now we have the option of persistenting sessions:

If we select the Remember me option we’ll find we don’t need to log in again after closing and re-opening our browser. Session persistence… done! Was that easy or what!

Next let’s see how we can add registration confirmation and password resets.

Adding registration confirmation and password resets

We’ll handle registration confirmation and password resets in one go as the set-up steps are fairly similar for both.

We need a migration for the email confirmation functionality so let’s start by creating that.

Terminal
mix pow.extension.ecto.gen.migrations --extension PowEmailConfirmation

We’ll then run the migration.

Terminal
mix ecto.migrate

Next we need to update config.exs with the new extensions.

/config/config.exs …line 20
# Pow configuration
config :warehouse, :pow,
  user: Warehouse.Users.User,
  repo: Warehouse.Repo,
  web_module: WarehouseWeb,
  extensions: [PowPersistentSession, PowResetPassword, PowEmailConfirmation],
  controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks

Now we need to update user.ex, including the extensions and adding a changeset.

/lib/warehouse/users/user.ex
defmodule Warehouse.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema
  use Pow.Extension.Ecto.Schema,
    extensions: [PowResetPassword, PowEmailConfirmation]

  schema "users" do
    pow_user_fields()

    timestamps()
  end

  def changeset(user_or_changeset, attrs) do
    user_or_changeset
    |> pow_changeset(attrs)
    |> pow_extension_changeset(attrs)
  end
end

Finally we also need to add some routes.

/lib/warehouse_web/router.ex
defmodule WarehouseWeb.Router do
  use WarehouseWeb, :router
  use Pow.Phoenix.Router
  use Pow.Extension.Phoenix.Router, otp_app: :warehouse
  ...
  ...

  scope "/" do
    pipe_through :browser

    pow_routes()
    pow_extension_routes()
  end
  ...
  ...

We’ve added a new use statement (use Pow.Extension.Phoenix.Router, otp_app: :warehouse) and then updated the first scope section to include pow_extension_routes.

Before testing things out, let’s add a reset password link in our sign in form.

/lib/warehouse_web/templates/pow/session/new.html.eex …line 26
<div>
  <%= link "Reset password", to: Routes.pow_reset_password_reset_password_path(@conn, :new)  %>
</div>

<span><%= link "Register", to: Routes.pow_registration_path(@conn, :new) %></span>

Now if we start up our server we should be good to go.

Terminal
mix phx.server

We see our new Reset password link in the sign in form.

And if we click it we get:

Doh… so what has happened here? Well if you recall we are using customized versions of the Pow templates, i.e. we generated the templates and added web_module: WarehouseWeb to config.exs. This means that our newly added functionality also expects generated versus default templates. So we need to generate the templates.

Terminal
mix pow.extension.phoenix.gen.templates --extension PowResetPassword --extension PowEmailConfirmation

And with that, we should be good.

But wait, we have one more step left, if we submit the reset password form as things currently stand we’ll get.

As per the Pow README we also need to set-up mailer support. This is easy to do, the instructions in the README provide instructions for setting up a mock that will output our email contents to the console log. This works perfect for the purposes of development.

First we create the mailer.

Terminal
mkdir lib/warehouse_web/pow
touch lib/warehouse_web/pow/mailer.ex
/lib/warehouse_web/pow/mailer.ex
defmodule WarehouseWeb.Pow.Mailer do
  use Pow.Phoenix.Mailer
  require Logger

  def cast(%{user: user, subject: subject, text: text, html: html, assigns: _assigns}) do
    # Build email struct to be used in `process/1`

    %{to: user.email, subject: subject, text: text, html: html}
  end

  def process(email) do
    # Send email

    Logger.debug("E-mail sent: #{inspect email}")
  end
end

And then we need to update the Pow configuration in config.exs.

/config/config.exs …line 20
# Pow configuration
config :warehouse, :pow,
  user: Warehouse.Users.User,
  repo: Warehouse.Repo,
  web_module: WarehouseWeb,
  extensions: [PowPersistentSession, PowResetPassword, PowEmailConfirmation],
  controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks,
  mailer_backend: WarehouseWeb.Pow.Mailer

After a server restart everything is good.

Terminal
mix phx.server

Hitting submit yields the following:

And in our console we see:

Following the link in the console we get.

Sweet!

If we register a new user, we’ll see the registration confirmation functionality is now up and working as well.

Following the link in the console gives us:

So that does it for adding registration confirmation and password resets!

Let’s have a quick look at a few other customizations before finishing off for today.

Other customizations

One thing I’ve noticed is when signing out of the application the user is taken to the sign in page. I’d prefer if the user was redirected to the root of the application. We can use Callback routes to change this behaviour.

Callback routes

The list of available callback routes can be seen in the source code at: https://github.com/danschultzer/pow/blob/master/lib/pow/phoenix/routes.ex.

https://github.com/danschultzer/pow/blob/master/lib/pow/phoenix/routes.ex
...
...
@callback user_not_authenticated_path(Conn.t()) :: binary()
@callback user_already_authenticated_path(Conn.t()) :: binary()
@callback after_sign_out_path(Conn.t()) :: binary()
@callback after_sign_in_path(Conn.t()) :: binary()
@callback after_registration_path(Conn.t()) :: binary()
@callback after_user_updated_path(Conn.t()) :: binary()
@callback after_user_deleted_path(Conn.t()) :: binary()
@callback session_path(Conn.t(), atom(), list()) :: binary()
@callback registration_path(Conn.t(), atom()) :: binary()
@callback path_for(Conn.t(), atom(), atom(), list(), Keyword.t()) :: binary()
@callback url_for(Conn.t(), atom(), atom(), list(), Keyword.t()) :: binary()
...
...

We’re going to want to make use of the after_sign_out_path callback. We can do so by creating a pow.routes file.

Terminal
touch lib/warehouse_web/pow/routes.ex

And adding the following contents.

/lib/warehouse_web/pow/routes.ex
defmodule WarehouseWeb.Pow.Routes do
  use Pow.Phoenix.Routes
  alias WarehouseWeb.Router.Helpers, as: Routes

  def after_sign_out_path(conn), do: Routes.page_path(conn, :index)
end

All we’re doing above is indicating we want to go to the index of the page_controller on sign out.

We need another entry in config.exs as well, specifying the routes_backend configuration.

/config/config.exs …line 20
# Pow configuration
config :warehouse, :pow,
  user: Warehouse.Users.User,
  repo: Warehouse.Repo,
  web_module: WarehouseWeb,
  extensions: [PowPersistentSession, PowResetPassword, PowEmailConfirmation],
  controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks,
  mailer_backend: WarehouseWeb.Pow.Mailer,
  routes_backend: WarehouseWeb.Pow.Routes

Now if we restart the server and logout we’ll be redirected to the main page of our application instead of the sign in page.

Fantastic!

Customizing or adding flash messages

Another area of Pow that we can customize is the flash messages shown to the user. For instance, currently if the user is not signed in and tries to access a page that is protected, they are redirected to the sign in page and no flash message appears. We’ll add a message for this scenario.

The first step is to create a new module for our messages.

Terminal
touch lib/warehouse_web/pow/messages.ex

And then add a message for user_not_authenticated.

/lib/warehouse_web/pow/messages.ex
defmodule WarehouseWeb.Pow.Messages do
  use Pow.Phoenix.Messages
  # since we're using the reset password and email confirmation
  # extensions we need to include them as well
  use Pow.Extension.Phoenix.Messages,
   extensions: [PowResetPassword, PowEmailConfirmation]

  import WarehouseWeb.Gettext

  def user_not_authenticated(_conn), do: gettext("You need to sign in to see this page.")
end

The hex documentation provides a good reference for the messages available within Pow:

The source code is also a good reference and is located at: https://github.com/danschultzer/pow/blob/master/lib/pow/phoenix/messages.ex.

For the reset_password and email_confirmation extensions the code is at: https://github.com/danschultzer/pow/blob/master/lib/extensions/reset_password/phoenix/messages.ex.

https://github.com/danschultzer/pow/blob/master/lib/extensions/reset_password/phoenix/messages.ex
...
...
def email_has_been_sent(_conn), do: "An email with reset instructions has been sent to you. Please check your inbox."
def invalid_token(_conn), do: "The reset token has expired."
def password_has_been_reset(_conn), do: "The password has been updated."
...
...

and https://github.com/danschultzer/pow/blob/master/lib/extensions/email_confirmation/phoenix/messages.ex.

https://github.com/danschultzer/pow/blob/master/lib/extensions/email_confirmation/phoenix/messages.ex
...
...
def email_has_been_confirmed(_conn), do: "The email address has been confirmed."
def email_confirmation_failed(_conn), do: "The email address couldn't be confirmed."
def email_confirmation_required(_conn), do: "You'll need to confirm your e-mail before you can sign in. An e-mail confirmation link has been sent to you."
...
...

Like the other Pow customizations we need to update config.exs. We need to add a messages_backend entry.

/config/config.exs …line 20
# Pow configuration
config :warehouse, :pow,
  user: Warehouse.Users.User,
  repo: Warehouse.Repo,
  web_module: WarehouseWeb,
  extensions: [PowPersistentSession, PowResetPassword, PowEmailConfirmation],
  controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks,
  mailer_backend: WarehouseWeb.Pow.Mailer,
  routes_backend: WarehouseWeb.Pow.Routes,
  messages_backend: WarehouseWeb.Pow.Messages

Now with a server restart, when trying to access the ‘Products’ pages without signing in, we see the message we added:

Sweet!

Summary

So that’s it for our tour of Pow. I’d recommend you check out the README and the source code to gain a further understanding of how things work.

I’m really impressed with the way Pow is structured and how easy it is to use, set-up and customize. Hopefully you’ll find Pow useful for your projects as well!

Thanks for reading and I hope you enjoyed the post!



Comment on this post!