Authentication & Authorization
Phoenix authentication with phx.gen.auth, session management, and authorization patterns
Phoenix provides built-in authentication scaffolding via phx.gen.auth and a plug-based architecture for authorization.
phx.gen.auth
The quickest way to add authentication to a Phoenix project:
mix phx.gen.auth Accounts User usersThis generates:
| File | Purpose |
|---|---|
accounts.ex |
Context module for user accounts |
user.ex |
User schema |
user_token.ex |
Token schema for session/reset/confirm tokens |
user_session_controller.ex |
Session controller |
user_session_html.ex |
Login page template |
user_registration_controller.ex |
Registration controller |
user_registration_html.ex |
Registration page template |
user_reset_password_controller.ex |
Password reset controller |
user_confirmation_controller.ex |
Email confirmation controller |
user_auth.ex |
Authentication plugs |
routes |
Auth routes in router |
Generated routes
| Path | Method | Purpose |
|---|---|---|
/users/register |
GET/POST | Registration |
/users/log_in |
GET/POST | Login |
/users/log_out |
DELETE | Logout |
/users/settings |
GET/POST | Change email/password |
/users/reset_password |
GET/POST | Request password reset |
/users/reset_password/:token |
GET/POST | Reset password |
/users/confirm |
POST | Resend confirmation |
/users/confirm/:token |
GET | Confirm account |
Auth plugs
The generated UserAuth module provides plugs:
defmodule MyAppWeb.UserAuth do
use MyAppWeb, :verification
@doc "Logs the user in by storing the user in the session."
def log_in_user(conn, user, params \\ %{}) do
token = Accounts.generate_user_session_token(user)
conn
|> renew_session()
|> put_session(:user_token, token)
|> maybe_assign_remember_me(user, params)
end
@doc "Logs the user out."
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
if user_token, do: Accounts.delete_session_token(user_token)
conn
|> renew_session()
|> configure_session(renew: true)
|> redirect(to: "/")
end
@doc "Fetches the current user from the session."
def fetch_current_user(conn, _opts) do
{user_token, conn} = result(conn)
user = user_token && Accounts.get_user_by_session_token(user_token)
assign(conn, :current_user, user)
end
@doc "Redirects if user is authenticated."
def redirect_if_user_is_authenticated(conn, _opts) do
if conn.assigns[:current_user] do
conn
|> redirect(to: signed_in_path(conn))
|> halt()
else
conn
end
end
@doc "Requires authentication."
def require_authenticated_user(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
|> redirect(to: ~p"/users/log_in")
|> halt()
end
end
endProtecting routes
In the router
scope "/", MyAppWeb do
pipe_through [:browser, :require_authenticated_user]
live "/settings", UserSettingsLive
live "/dashboard", DashboardLive
end
scope "/", MyAppWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
live "/users/register", UserRegistrationLive
live "/users/log_in", UserLoginLive
endIn a controller or LiveView
# Controller
defmodule MyAppWeb.DashboardController do
use MyAppWeb, :controller
plug :require_auth
def index(conn, _params), do: render(conn, :index)
defp require_auth(conn, _) do
if conn.assigns[:current_user] do
conn
else
conn |> redirect(to: ~p"/users/log_in") |> halt()
end
end
end
# LiveView — mount/3
def mount(_params, _session, socket) do
if socket.assigns[:current_user] do
{:ok, socket}
else
{:ok, redirect(socket, to: ~p"/users/log_in")}
end
endAuthorization (role-based)
Authorization is separate from authentication. A common pattern:
defmodule MyAppWeb.Plugs.Authorize do
import Plug.Conn
import Phoenix.Controller
def init(opts), do: opts
def call(conn, roles: roles) do
user = conn.assigns[:current_user]
if user && user.role in roles do
conn
else
conn
|> put_status(:forbidden)
|> put_view(html: MyAppWeb.ErrorHTML)
|> render("403.html")
|> halt()
end
end
end
# In the router
scope "/admin", MyAppWeb.Admin do
pipe_through [:browser, :require_authenticated_user, :authorize, roles: [:admin]]
resources "/users", UserController
endPolicy-based authorization (Canada-style)
defmodule MyAppWeb.Policy do
def can?(%{role: :admin}, _action, _resource), do: true
def can?(%{role: :editor}, :edit, %Post{author_id: id}, id), do: true
def can?(%{role: :editor}, :edit, _resource), do: false
def can?(_, _, _), do: false
end
# Usage in LiveView
def handle_event("edit", _, %{assigns: %{current_user: user, post: post}} = socket) do
if MyAppWeb.Policy.can?(user, :edit, post) do
{:noreply, push_navigate(socket, to: ~p"/posts/#{post.id}/edit")}
else
{:noreply, put_flash(socket, :error, "Not authorized")}
end
endCurrent user in LiveView
The on_mount callback injects the current user into LiveView assigns:
defmodule MyAppWeb.UserAuth do
def on_mount(:ensure_authenticated, _params, session, socket) do
case session_user(session) do
nil ->
{:halt, redirect(socket, to: ~p"/users/log_in")}
user ->
{:cont, assign(socket, :current_user, user)}
end
end
def on_mount(:mount_current_user, _params, session, socket) do
{:cont, assign_new(socket, :current_user, fn -> session_user(session) end)}
end
end
# In the router
live_session :authenticated, on_mount: [{MyAppWeb.UserAuth, :ensure_authenticated}] do
live "/dashboard", DashboardLive
end
live_session :default, on_mount: [{MyAppWeb.UserAuth, :mount_current_user}] do
live "/", PageLive
endAPI authentication (Bearer tokens)
For API endpoints, use token-based authentication:
defmodule MyAppWeb.API.AuthPlug do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
{:ok, user} <- MyApp.Accounts.fetch_user_by_api_token(token) do
assign(conn, :current_user, user)
else
_ ->
conn
|> put_status(:unauthorized)
|> Phoenix.Controller.json(%{error: "Unauthorized"})
|> halt()
end
end
end
# In the router
scope "/api", MyAppWeb.API do
pipe_through [:api, MyAppWeb.API.AuthPlug]
resources "/posts", PostController, except: [:new, :edit]
endEmail verification & password reset
The generated auth includes mailer integration stubs. Configure your mailer:
# config/config.exs
config :my_app, MyApp.Mailer, adapter: Swoosh.Adapters.Local
# config/prod.exs
config :my_app, MyApp.Mailer, adapter: Swoosh.Adapters.Mailgun,
api_key: System.fetch_env!("MAILGUN_API_KEY"),
domain: System.fetch_env!("MAILGUN_DOMAIN")