jonnifyelixir
F6 / F6.08 / auth

F6.08 · part 1 of 3

Sessions & authentication

Authentication in Phoenix is not a framework you bolt on — it is mostly code you generate and then own. mix phx.gen.auth Accounts User users writes an Accounts context (exactly the F6.04 shape), a User schema with a hashed-password field, a token schema, the registration and login LiveViews and controllers, and the session plumbing — then you read it, run its migration, and adjust it like any other code. Three pieces carry the weight. The session is a signed cookie: on login the app verifies the password with bcrypt and stores a token in the cookie, not the password and not the user struct. A plug, fetch_current_user, runs in the browser pipeline on every request, reads that token, and puts current_user into conn.assigns so controllers and templates can see it. And because a LiveView holds a long-lived socket rather than a request, it enforces auth with on_mount — a hook that runs before the socket connects and can halt and redirect. Password hashing, CSRF, and token expiry come built in; what you decide is which routes require a user.

Four parts: the three moving pieces, the login flow end to end, protecting routes in the router, and enforcing auth in a LiveView with on_mount.

Session, plug, on_mount

Three pieces make up auth. The session is a signed cookie carrying a token. A plug reads it and loads current_user into the connection. on_mount does the same enforcement for a LiveView, before it connects. Select one.

Authentication · select one

THE SESSION a signed cookie carrying a token — not the password state a plug fetch_current_user — loads the user into conn.assigns per request on_mount checks auth before a LiveView mounts — cont or halt per socket

piece: the session

is: a signed cookie with a token

Storing a token rather than the user means the cookie carries no secrets and can be invalidated server-side. The password itself is only ever compared as a bcrypt hash — it is never stored or sent in the session.

The login flow

Login is one pass through the layers you already know. The form submits an email and password; the Accounts context looks the user up and verifies the password with bcrypt; on success a token is written into the session cookie. On every later request the fetch_current_user plug reads the cookie and assigns current_user, so a protected route checks that assign.

log in → token in cookie → plug loads the user

LOGIN (once) submit email + password Accounts: verify password (bcrypt) put token in the session cookie EVERY LATER REQUEST request carries the cookie plug: fetch_current_user conn.assigns.current_user is set the token, written once, identifies the user on every request that follows a protected route checks current_user; no user, no entry

The lookup-and-verify step is plain context code — get_user_by_email_and_password/2 returns the user or nil. The web layer never touches password hashes; it asks the domain and trusts the answer.

Protecting routes

The generator adds fetch_current_user to the :browser pipeline so every page knows the current user, and a require_authenticated_user plug you add to a pipeline for routes that demand one. Pages stay public by default; you opt specific scopes into requiring a login — the same pipeline composition from F6.02.

# the generated plug puts current_user in conn.assigns on every request
pipeline :browser do
  plug :fetch_session
  plug :fetch_current_user
end

# these routes require a logged-in user; public routes stay outside
scope "/", PortalWeb do
  pipe_through [:browser, :require_authenticated_user]
  live "/catalog", CatalogLive
end

Authorization — who may do what — is a separate question you answer in the context with the current_user. The router handles only authentication: is there a user at all.

Enforcing it in LiveView

A protected LiveView cannot rely on a request-time plug for its whole life, so it uses on_mount: a hook grouped under a live_session that runs as the LiveView mounts. It loads the user from the session and returns {:cont, socket} to proceed or {:halt, redirect(...)} to bounce an anonymous visitor — before the socket connects or any data loads.

# group protected LiveViews and run an on_mount hook as they mount
live_session :authenticated,
  on_mount: [{PortalWeb.UserAuth, :ensure_authenticated}] do
  live "/catalog", CatalogLive
end

# in PortalWeb.UserAuth
def on_mount(:ensure_authenticated, _params, session, socket) do
  socket = mount_current_user(socket, session)

  if socket.assigns.current_user do
    {:cont, socket}
  else
    {:halt, redirect(socket, to: ~p"/users/log_in")}
  end
end

requests

A plug loads current_user per request, in the pipeline.

sockets

on_mount enforces it per LiveView, before connecting.

Next: releases & config — with auth in place, the app needs to be packaged to run on a server, with its secrets supplied at boot.