Psst. It would be super cool if you could try the new Phoenix Boilerplate!

Try now

Tutorial

View on Github

Multi-tenancy and authentication with Pow

This post was updated 01 May

authenticationphoenixpow

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 nae 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:


What are you working on?

If you want, you can send me a link to your Phoenix or Phoenix LiveView project so. Lets connect on Twitter or Linkedin.

- Andreas Eriksson, web developer since 2005

Related Tutorials

Published 16 Mar

Multi-tenancy and Phoenix - Part 2

powauthenticationmultitenancyphoenix

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

Published 30 Jul

Getting started with GraphQL and Absinthe in Phoenix

phoenixapiabsinthegraphql

In the last tutorial, there I had an app with a simple rest api that was authenticated with Guardian and Json Web Token. In this tutorial, I will go..