DEV Community

Cover image for Learn Elixir and Phoenix: Add authentication
Oliver Andrich
Oliver Andrich

Posted on • Originally published at on

Learn Elixir and Phoenix: Add authentication

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"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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


    create unique_index(:users, [:email])
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

    create unique_index(:users, [:email_confirmation_token])
Enter fullscreen mode Exit fullscreen mode

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


  def changeset(user_or_changeset, attrs) do
    |> pow_changeset(attrs)
    |> pow_extension_changeset(attrs)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  scope "/" do
    pipe_through :browser


  # ... other scopes

  scope "/protected", ReadItLaterWeb do
    pipe_through [:browser, :protected]

    get "/", PageController, :protected_index
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
      from: "",
      subject: subject,
      html_body: html,
      text_body: text

  @impl true
  def process(email) do
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.


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.


References & Credits

Discussion (3)

wulymammoth profile image

Nice post, Oliver -- I use Pow in my current project, but when I was looking through the issues on GitHub, someone asked about how it'll work with the impending auth provided by core devs, namely Jose, and here we are. It's nearly done in case you're not familiar. It's definitely not as full-featured as Pow with different OAuth2 provider strategies. Def check it out if you haven't already:

oliverandrich profile image
Oliver Andrich Author

Thanks for the hint. I will definitely look into it. And it is always good to have these basics readily available in the core. This was one of the aspects that got me hooked on Django many years ago. I have the feeling that I am now hooked on Elixir and Phoenix. :)

thefactus profile image
Pablo Bello

Well done!
I didn't know about Pow, and it seems to be amazing.

Thank you :)