Tutorial

Multi-tenancy and authentication with Pow

This post was updated 01 May - 2020

authentication phoenix pow

I basically model every app with multi-tenancy in mind. It is way easier to do it while building than to implement it as an after thought.

Also, Pow has risen to be my favourite authentication library but it will require some minor tweaks to get it working the way I want it to.

So in this tutorial I will show you how I usually sets this up so that when a user registers, I can also register an account that is associated with the user.

Fort this tutorial, I have already set up a page that should be restricted and requiring a signup.

STEP 1 - Generate the Account

First I want to generate the accounts feature with its own context and Account module. I need the name of the account but if you want address, phone etc, you can add more fields.

mix phx.gen.context Accounts Account accounts name

As always, I want to make changes to the migration file.

  1. I want to make the name required so I don't accidentally store a null value.
  2. I want to make the name unique, to avoid confusion later on.
# priv/repo/migrations/20200313082034_create_accounts.exs

defmodule Tutorial.Repo.Migrations.CreateAccounts do
  use Ecto.Migration

  def change do
    create table(:accounts) do
      add :name, :string, null: false

      timestamps()
    end
    create unique_index(:accounts, [:name])
  end
end

In the Account changeset I just need to make sure that we have the same unique_constraint

# lib/tutorial/accounts/account.ex
defmodule Tutorial.Accounts.Account do
  use Ecto.Schema
  import Ecto.Changeset

  schema "accounts" do
    field :name, :string

    timestamps()
  end

  @doc false
  def changeset(account, attrs) do
    account
    |> cast(attrs, [:name])
    |> validate_required([:name])
    |> unique_constraint(:name, name: :accounts_name_index)
  end
end

STEP 2 - Install Pow

defp deps do
  [
    # ...
    {:pow, "~> 1.0.18"}
  ]
end

And then install the package by:

mix deps.get

Pow comes with its own generator(s) so I can get started with running:

mix pow.install

This will generate a migration file and a user module. It also generates some instructions for how to integrate this into the app.

First I want to update the migrations file. And what I want to do is to add an account_id column that references the accounts column.

# priv/repo/migrations/20200315124813_create_users.exs
defmodule Tutorial.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :account_id, references(:accounts, on_delete: :delete_all), null: false

      add :email, :string, null: false
      add :password_hash, :string

      timestamps()
    end

    create index(:users, [:account_id])
    create unique_index(:users, [:email])
  end
end

And run migrations with

mix ecto.migrate

Next step is to configure how to setup Pow.

# config/config.exs
config :tutorial, :pow,
  user: Tutorial.Users.User,
  repo: Tutorial.Repo

After that is done, restart the server. Hit ctrl-c twice and start the server.

Next step in the instructions is to add Pow.Plug.Session plug to lib/tutorial_web/endpoint.ex after plug Plug.Session

# lib/tutorial_web/endpoint.ex
defmodule TutorialWeb.Endpoint do
  ...
  plug Plug.Session, @session_options
  plug Pow.Plug.Session, otp_app: :tutorial

  plug TutorialWeb.Router
end

And the final step is to make the changes to the router.

# lib/tutorial_web/router.ex
defmodule TutorialWeb.Router do
  use TutorialWeb, :router
  use Pow.Phoenix.Router

  # ... pipelines

  scope "/" do
    pipe_through :browser

    pow_routes()
  end

  # ... routes
end

If I run mix phx.routes | grep 'pow' now I can see the new routes that Pow has given me.

However, if I visit http://localhost:4000/private that page is still accessible. What I want is to be redirected to http://localhost:4000/session/new.

To add that, I need to to more changes to my routes file. I will add a new pipeline called :protected and a new scope that uses that pipeline. I will also make sure to move my route inside that.

# lib/tutorial_web/router.ex
pipeline :protected do
  plug Pow.Plug.RequireAuthenticated, error_handler: Pow.Phoenix.PlugErrorHandler
end

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

  get "/private", PageController, :private
end

With that in place, and I refresh the page, I now get redirected to http://localhost:4000/session/new?request_path=%2Fprivate

STEP 3 - User registration and customize the form

If I visit the page http://localhost:4000/registration/new I notice two things. The template and styling is totally off. That is because I use Tailwind CSS. Also, if I try to register, I get this error:

And the reason is of course that I added a database reference. The behaviour I want here is to actually create the account the same time as the user register. I also want a field for the account name.

First. I will lay the groundwork in the user module. Out of the box, I get a user module from Pow that look like this:

defmodule Tutorial.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema

  schema "users" do
    pow_user_fields()

    timestamps()
  end
end

I want to do 4 changes:

  1. Add the relationship to the Accounts
  2. Add a virtual attribute for the account name
  3. Add changeset for more control
  4. Create account if it doesn't exist
defmodule Tutorial.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema

  import Ecto.Changeset
  alias Tutorial.Accounts

  schema "users" do
    pow_user_fields()

    field :account_name, :string, virtual: true

    belongs_to :account, Tutorial.Accounts.Account

    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> pow_changeset(attrs)
    |> cast(attrs, [:account_name])
    |> validate_required([:account_name])
    |> create_account_for_new_user(user)
    |> assoc_constraint(:account)
  end

  defp create_account_for_new_user(%{valid?: true,  changes: %{account_name: account_name}} = changeset, %{account_id: nil} = user) do
    with {:ok, account} <- Accounts.create_account(%{name: account_name}) do
      put_assoc(changeset, :account, account)
    else
      _ -> changeset
    end
  end
  defp create_account_for_new_user(changeset, _), do: changeset
end

Next step here is to generate and modify templates. Run the task:

mix pow.phoenix.gen.templates

Also here, I get some configuration instructions. I need to open the config again ans specify my web module TutorialWeb:

# config/config.exs
config :tutorial, :pow,
  user: Tutorial.Users.User,
  repo: Tutorial.Repo,
  web_module: TutorialWeb

And as before, I need to restart the server when making changes to the config.

I could also see the list of the files being generated. The file I want to edit now is the file I use for a new registration. I want to add the :account_name inside the form

# lib/tutorial_web/templates/pow/registration/new.html.eex
  ...
  <%= label f, :account_name %>
  <%= text_input f, :account_name %>
  <%= error_tag f, :account_name %>
  ...

Note that I could as well have used fields for and utilize the Account changeset. I just want to show another approach.

When I fill in the form now including the account name, I can now successfully register and got to the page localhost:4000/private.

Just to see that all works, I can now change the private.html.eex and reference @current_user:

<!-- lib/tutorial_web/templates/page/private.html.eex -->
...

Welcome <%= @current_user.email %>

And when I refresh the page I will see the email for the current user:

Related Tutorials

Published 04 May - 2021
Updated 05 May - 2022

How to combine Phoenix LiveView with Alpine.js

No matter how great Phoenix LiveView is, there is still some use case for sprinking some JS in your app to improve UX. For example, tabs, dropdowns,..

Published 16 Mar - 2020
Updated 01 May - 2020

Multi-tenancy and Phoenix - Part 2

In the previous tutorial, I wrote how to set up multi-tenancy with Phoenix and Pow. In this tutorial, I will show how I scope resources the current ..