Tutorial
Multi-tenancy and authentication with Pow
This post was updated 27 Mar
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.
- I want to make the name required so I don’t accidentally store a null value.
- 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:
- Add the relationship to the Accounts
- Add a virtual attribute for the account name
- Add changeset for more control
- 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: