Sep 12, 2019

Customizing Pow

Pow is an excellent authentication solution for Phoenix HTML applications and APIs. It provides a solid, comprehensive bundle of features out of the box; but what if you need customized behaviour? In this post, we’ll look at how easy it is to customize the default functionality of Pow.

A common area where some customization may be desired is during authentication. This is the scenario we’ll look at today. We want Pow to handle the concept of a disabled user. When a user is disabled we don’t want them to be able to log in.

A pretty simple requirement, so let’s get at it!

Getting started

To save some time in getting a boiler plate application set up, we’ll use the code from an earlier post, Phoenix authentication with Pow - Part 1, as a starting point. The code contains a basic Pow configured application, along with some scaffolding, which we can ignore for the purposes of this post.

So let’s grab the code.

Clone the Repo:

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

We’ll create a branch for today’s work.

Terminal
git checkout -b customize-authentication

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.

Nothing fancy, basically just the application you would get by running mix phx.new and setting up Pow. All we care about for today is the sign in process so let’s follow the sign up link and create a user to work with.

After signing up, we’ll see that we’ve been logged into our application.

Time to add the disable user functionality.

Adding the disable user functionality

The way our disable functionality will work, is we will have a column in the users table indicating whether a user is disabled or not. Once we have the column in the database, we’ll update Pow to take this column into account when authenticating.

Making the database updates

So the first thing we’ll need to do is add a new column to the users table; let’s create a new migration.

Terminal
mix ecto.gen.migration add_is_disabled_to_users

And update the migration as follows.

/priv/repo/migrations/_add_is_disabled_to_users.exs
defmodule Warehouse.Repo.Migrations.AddIsDisabledToUsers do
  use Ecto.Migration

  def change do
    alter table(:users) do
      add :is_disabled, :boolean, null: false, default: false
    end
  end
end

Pretty simple, we’re adding a new non-null column to users, and defaulting the value of the column to false.

Now we’ll run the migration.

Terminal
mix ecto.migrate

And we also need to add the new column to our user schema.

/lib/warehouse/users/user.ex
defmodule Warehouse.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema

  schema "users" do
    pow_user_fields()

    field :is_disabled, :boolean

    timestamps()
  end
end

So that’s it for the database and schema updates, onto Pow!

Updating Pow

So how can we get Pow to take into account our new column? Turns out this is pretty easy! Looking at the Pow source, the context module contains some comments which tell us exactly what we need to do:

defmodule Pow.Ecto.Context do
  @moduledoc """
  Handles pow users context for user.

  ## Usage

  This module will be used by pow by default. If you
  wish to have control over context methods, you can
  do configure `lib/my_project/users/users.ex`
  the following way:

      defmodule MyApp.Users do
        use Pow.Ecto.Context,
          repo: MyApp.Repo,
          user: MyApp.Users.User

        def create(params) do
          pow_create(params)
        end
      end

  Remember to update configuration with `users_context: MyApp.Users`.

  The following Pow methods can be accessed:
    * `pow_authenticate/1`
    * `pow_create/1`
    * `pow_update/2`
    * `pow_delete/1`
    * `pow_get_by/1`
    ...
    ...

Nice! So we’re going to want to use pow_authenticate/1, let’s have a look at the spec for authenticate.

...
...
@doc """
Finds a user based on the user id, and verifies the password on the user.

User schema module and repo module will be fetched from the config. The user
id field is fetched from the user schema module.

The method will return nil if either the fetched user, or password is nil.

To prevent timing attacks, a blank user struct will be passed to the
`verify_password/2` method for the user schema module to ensure that the
the response time will be equal as when a password is verified.
"""
@spec authenticate(map(), Config.t()) :: user() | nil
...
...

Ok, so it looks like the method returns a user on valid authentication and nil otherwise.

We now have enough information to implment our disabled user functionality.

The first step is to create the custom context.

Terminal
touch lib/warehouse/users/users.ex
/lib/warehouse/users/users.ex
defmodule Warehouse.Users do
  use Pow.Ecto.Context,
    repo: Warehouse.Repo,
    user: Warehouse.Users.User

  def authenticate(params) do
    user = pow_authenticate(params)
    if user != nil && user.is_disabled do
      nil
    else
      user
    end
  end
end

What we’ve done is we now check the is_disabled flag when a user is returned from pow_authenticate\1. If the flag is set we return nil instead of the user… simple!

Before testing things out, we need to update the Pow configuration.

/config/config.exs …line 20
# Pow configuration
config :warehouse, :pow,
  user: Warehouse.Users.User,
  repo: Warehouse.Repo,
  web_module: WarehouseWeb,
  users_context: Warehouse.Users

Testing things out

Let’s start up our server to see our changes in action.

Terminal
mix phx.server

Disable the user

Normally you might create some sort of admin application for managing users, but for the purposes of this post we’ll stick with plain old SQL. I’m using TablePlus, but you can use any SQL client you prefer.

SQL
update users set is_disabled = true
where email = 'bob@example.com'

If we now attempt to sign-in again, we’ll see:

Success! Looks like our changes have worked!

If we switch the is_disabled flag back to false, we’ll once again be able to login.

SQL
update users set is_disabled = false
where email = 'bob@example.com'

Summary

The design of Pow makes customization of the default behaviour very straight forward and clean. This is fantastic as it means we don’t need to add a bunch of hacks to adapt Pow to our particular requirements… always a good thing!

Thanks for reading and I hope you enjoyed the post!



Comment on this post!