Tutorial

Multi-tenancy and authentication with Pow

This post was updated 27 Mar

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

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 set this up so that when a user registers, I can also register an account that is associated with the user.

For 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 MyApp.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/my_app/accounts/account.ex
defmodule MyApp.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 MyApp.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 set up Pow.

# config/config.exs
config :my_app, :pow,
user: MyApp.Users.User,
repo: MyApp.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/my_app_web/endpoint.ex after plug Plug.Session

# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
...
plug Plug.Session, @session_options
plug Pow.Plug.Session, otp_app: :my_app
plug MyAppWeb.Router
end

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

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :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 make 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/my_app_web/router.ex
pipeline :protected do
plug Pow.Plug.RequireAuthenticated, error_handler: Pow.Phoenix.PlugErrorHandler
end
scope "/", MyAppWeb 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 registers. 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 looks like this:

defmodule MyApp.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 MyApp.Users.User do
use Ecto.Schema
use Pow.Ecto.Schema
import Ecto.Changeset
alias MyApp.Accounts
schema "users" do
pow_user_fields()
field :account_name, :string, virtual: true
belongs_to :account, MyApp.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 and specify my web module MyAppWeb:

# config/config.exs
config :my_app, :pow,
user: MyApp.Users.User,
repo: MyApp.Repo,
web_module: MyAppWeb

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/my_app_web/templates/pow/registration/new.html.heex -->
...
<.input field={@changeset[:account_name]} type="text" label="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 go to the page localhost:4000/private.

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

<!-- lib/my_app_web/templates/page/private.html.heex -->
...
Welcome {@current_user.email}

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

Related Tutorials

NEW
Published 11 Apr

Rendering an Activity Feed in Phoenix LiveView

In the previous tutorial, we built an automatic activity tracking system that records events whenever a tracked schema is inserted, updated, or dele..

Published 16 Mar - 2020
Updated 27 Mar

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