Tutorial
Multi-tenancy and authentication with Pow
This post was updated 01 May - 2020
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.
- 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 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:
- 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 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: