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
orBamboo.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.exs
to 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
Discussion
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: github.com/aaronrenner/phx_gen_auth
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. :)
Well done!
I didn't know about Pow, and it seems to be amazing.
Thank you :)