Learn Elixir and Phoenix: Add authentication

Here is the next episode of my journey to learn Elixir and Phoenix. The topic is user registration and authentication with pow.

Post number three in my series to learn Elixir and Phoenix. This time I will add user registration and authentication to the application. I decided to use Pow for this, as it seems to be the most complete and modern library for this purpose. It provides all that I need in one package, which makes it easy to avoid integration issues. Coming from Django, I enjoy having a full-fledged solution for user management that I can just use.

The screenshot above shows the landing page of the project after completing the following steps.

Planned changes

  • Add the dependencies.
  • Add the user model.
  • Add the controllers, views and templates for sign in, sign up and password reset. The “sign up” and “password reset” actions also include an email workflow.
  • Add a protected resource.

Requirement: To follow all the steps in this tutorial, you need to have access to a mail server. I use Mailhog as a local mail server during development.

Step 1: Add dependencies

The first step in this tutorial is to add the required dependencies to the project, which is pretty easy and straight forward for an all-in-one package like Pow. Besides the core package, we need one to send emails. I picked bamboo and the bamboo_smtp transport for this task.

Open the mix.exs file and add pow, bamboo and bamboo_smtp to the list of dependencies like it is shown in the following code fragment.

defp deps do [ # ... {:pow, "~> 1.0.20"}, {:bamboo, "~> 1.5"}, {:bamboo_smtp, "~> 2.1.0"} ] end

Afterwards, run the command mix deps.get to install the packages locally. That’s it.

Step 2: Create the user model

Pow provides a handy mix task to get you started and scaffold a basic user model and database migration for it.

mix pow.install

The above command created two files:

  • lib/read_it_later/users/user.ex
  • priv/repo/migrations/TIMESTAMP_create_users.ex.

The following code block shows the user model and schema definition from lib/read_it_later/users/user.ex. It just adds the necessary fields for Pow, that means email address, password hash, and timestamps.

defmodule ReadItLater.Users.User do use Ecto.Schema use Pow.Ecto.Schema schema "users" do pow_user_fields() timestamps() end end

The database migration defined in priv/repo/migrations/TIMESTAMP_create_users.ex matches the above schema definition and creates the actual database table in the PostgreSQL database.

defmodule ReadItLater.Repo.Migrations.CreateUsers do use Ecto.Migration def change do create table(:users) do add :email, :string, null: false add :password_hash, :string timestamps() end create unique_index(:users, [:email]) end end

Coming from the Django framework, I was a bit surprised by the database migrations in Phoenix, as they usually don’t provide an explicit up and down function. Ecto is capable of inferring what to do when going forward or backwards on the migration path. But this is a topic for a separate post, and I also have to research this a bit more.

Step 3: Extend the user model to support email confirmation and password reset

As mentioned above, besides the basic functionality of sign up and sign in, we also want to provide password recovery and email confirmation.

At least the email confirmation extension needs some extra database fields to track if an email address has been confirmed or not. The following command creates a database migration adding the necessary fields.

mix pow.extension.ecto.gen.migrations \ --extension PowResetPassword \ --extension PowEmailConfirmation

The resulting migration file priv/repo/migrations/TIMESTAMP_add_pow_email_confirmation_to_users.exs looks like that.

defmodule ReadItLater.Repo.Migrations.AddPowEmailConfirmationToUsers do use Ecto.Migration def change do alter table(:users) do add :email_confirmation_token, :string add :email_confirmed_at, :utc_datetime add :unconfirmed_email, :string end create unique_index(:users, [:email_confirmation_token]) end end

It adds three new fields and an index to the database table users. To apply the database migration we have to run mix ecto.migrate.

As a final task in this section, we have to extend the user model to support the two extensions by adding a use statement and overwriting the changeset function in order to apply the validation rules of the extensions.

The use statement imports the pow_extension_changeset function into the module and also extends the functionality of the pow_user_fields function, so that it also adds the fields from the extensions.

defmodule ReadItLater.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

Step 4: Configure pow

After creating the user model, we need to configure pow in the config/config.exs file by adding the content of the following code block. This code does the following:

  • Connect pow with the read_it_later application.
  • Tell pow which module provides the user model.
  • Tell pow which Ecto database repository to use.
  • Which extensions should be loaded.
  • Where to find the controller for the callbacks required by the email confirmation and password recovery extensions.
  • In which web module to search for the custom templates for its views.
use Mix.Config # ... existing config config :read_it_later, :pow, user: ReadItLater.Users.User, repo: ReadItLater.Repo, extensions: [PowResetPassword, PowEmailConfirmation], controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks, web_module: ReadItLaterWeb # ... import_config

Step 5: Configure the endpoint

We are implementing a classic session-based authentication workflow. For that, we need to activate the pow session handling in lib/read_it_later_web/endpoint.ex.

defmodule ReadItLaterWeb.Endpoint do use Phoenix.Endpoint, otp_app: :read_it_later # ... plug Plug.Session, @session_options plug Pow.Plug.Session, otp_app: :read_it_later plug ReadItLaterWeb.Router end

Step 6: Set up the routes

In this step we will add three things to your router definition.

  • Import routing additions from Pow and the used extensions.
  • A new pipeline for protected pages/routes/resources.
  • A new scope for the pow routes to sign up, sign in, retrieve your password and verify your email address.
  • A new protected route just to showcase what we built. As an example we add another function to the PageController generate during the project setup.

These are the changes that need to be applied to lib/read_it_later_web/router.ex to cover the above topics.

defmodule ReadItLaterWeb.Router do use ReadItLaterWeb, :router use Pow.Phoenix.Router use Pow.Extension.Phoenix.Router, extensions: [PowResetPassword, PowEmailConfirmation] # ... other pipelines pipeline :protected do plug Pow.Plug.RequireAuthenticated, error_handler: Pow.Phoenix.PlugErrorHandler end scope "/" do pipe_through :browser pow_routes() pow_extension_routes() end # ... other scopes scope "/protected", ReadItLaterWeb do pipe_through [:browser, :protected] get "/", PageController, :protected_index end end

If you now run the command mix phx.routes on the shell, you see the following output with all the routes for your web application. Everything is in place to register a new account, validate an email address, sign in and get a password reminder. Personally, I also highly appreciate how they implement a REST-ful URL scheme.

pow_session_path GET /session/new Pow.Phoenix.SessionController :new pow_session_path POST /session Pow.Phoenix.SessionController :create pow_session_path DELETE /session Pow.Phoenix.SessionController :delete pow_registration_path GET /registration/edit Pow.Phoenix.RegistrationController :edit pow_registration_path GET /registration/new Pow.Phoenix.RegistrationController :new pow_registration_path POST /registration Pow.Phoenix.RegistrationController :create pow_registration_path PATCH /registration Pow.Phoenix.RegistrationController :update PUT /registration Pow.Phoenix.RegistrationController :update pow_registration_path DELETE /registration Pow.Phoenix.RegistrationController :delete pow_reset_password_reset_password_path GET /reset-password/new PowResetPassword.Phoenix.ResetPasswordController :new pow_reset_password_reset_password_path POST /reset-password PowResetPassword.Phoenix.ResetPasswordController :create pow_reset_password_reset_password_path PATCH /reset-password/:id PowResetPassword.Phoenix.ResetPasswordController :update PUT /reset-password/:id PowResetPassword.Phoenix.ResetPasswordController :update pow_reset_password_reset_password_path GET /reset-password/:id PowResetPassword.Phoenix.ResetPasswordController :edit pow_email_confirmation_confirmation_path GET /confirm-email/:id PowEmailConfirmation.Phoenix.ConfirmationController :show page_path GET / ReadItLaterWeb.PageController :index page_path GET /protected ReadItLaterWeb.PageController :protected_index

Step 7: Adapting the templates

If I had stuck to the default CSS files from the Phoenix default project, everything would be set now. Pow includes default templates for sign up, sign in, password recovery, password change and so on. But they are not using Tailwind CSS and don’t comply with the minimal screen design I created.

To overwrite the default templates, you have to run the following mix task.

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

This command creates the views and template files for registration, sign in and password reset in the folders templates and views. The code can be tweaked manually now. I won’t outline my changes in this post. Just have a look at the GitHub repository of this learning project.

├── templates │ ├── ... │ ├── pow │ │ ├── registration │ │ │ ├── edit.html.eex │ │ │ └── new.html.eex │ │ └── session │ │ └── new.html.eex │ └── pow_reset_password │ └── reset_password │ ├── edit.html.eex │ └── new.html.eex └── views ├── ... ├── pow │ ├── registration_view.ex │ └── session_view.ex └── pow_reset_password └── reset_password_view.ex

Step 8: Configure email delivery

Both password recovery and email validation, send emails. So we need to add a mailer to the project. The official tutorial describes how to hook up a dummy mailer, that just logs to the console. Personally, I prefer to connect to a „real“ mailserver like Mailhog.

Add the mailer

We added bamboo as a dependency in step 1 of this post. Now we need to connect it to pow and configure it.

First we create the file lib/read_it_later_web/pow/mailer.ex with the following content. This mailer is a thin layer around bamboo and provides just the clue code to connect pow and bamboo.

defmodule ReadItLaterWeb.Pow.Mailer do use Pow.Phoenix.Mailer use Bamboo.Mailer, otp_app: :read_it_later import Bamboo.Email @impl true def cast(%{user: user, subject: subject, text: text, html: html}) do new_email( to: user.email, from: "reading-list@example.com", subject: subject, html_body: html, text_body: text ) end @impl true def process(email) do deliver_now(email) end end

I think the functionality of the functions cast and process is pretty obvious. But I like to outline three different lines in this code block, that I found interesting.

  • The first highlighted line imports again Bamboo.Mailer into our mailer module. But it also connects bamboo to the OTP application.
  • I picked the second highlight because it took me a while to find out what the difference is between use, import and alias. I am still not 100% sure when to use what, but it is getting better and better. I strongly advise reading the chapter of the elixir guides on this topic.
  • The third highlight is something exciting. Especially when you came from an object-oriented language and learned that Elixir doesn’t use classes and objects. The above code overwrites a function imported from Bamboo.Mailer or Bamboo.Email. How? It took me a while to understand what is happening. Then I stumbled across the documentation of defoverridable. Check it out.

Connect the mailer to pow

In step 4 of this post, we configured pow in the file config/config.exs. We have to add two more lines to this configuration now so that it matches the following code. mailer_backend connects the mailer we just created to Pow. web_module_mailer tells pow where to look for the email templates.

config :read_it_later, :pow, user: ReadItLater.Users.User, repo: ReadItLater.Repo, extensions: [PowResetPassword, PowEmailConfirmation], controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks, web_module: ReadItLaterWeb, mailer_backend: ReadItLaterWeb.Pow.Mailer, web_module_mailer: ReadItLaterWeb

Configure the mailer in dev mode

Add the following lines to your config/dev.exsto tell bamboo to use your local mail server listening on port 1025. If you use a different solution than Mailhog or a public mailserver, you have to adapt the settings. Check the bamboo_smtp documentation for all the settings available.

config :read_it_later, ReadItLaterWeb.Pow.Mailer, adapter: Bamboo.SMTPAdapter, server: "localhost", port: 1025

Customize email templates

Of course, you can also change the mailer templates, but I will skip this step for the moment and focus on the web application parts.

Summary

We are done now! Everything planned is in place.

  • Added a basic user model.
  • New users can sign up and have to validate their email address.
  • Existing users can log in or reset their password.
  • A simple, protected page was added.
  • The application can send emails.

I am pretty happy with this episode. I learned a lot about Phoenix and Elixir again even though I spent most of the time scaffolding, configuring and researching the generated code.

I also skipped one thing I promised in the beginning of the post. I wanted to add fields for username, first and last name. This will be a separate post on this series. Actually, it will be the next post.

The code

I share the code to this project on github. Every post gets its own tag on the repository, so you can easily switch to the code of a given post.

oliverandrich/learn-elixir-and-phoenix-project


References & Credits