Authentication

You can of course bolt a third-party authentication library onto a Phoenix project, but in 1.8 you usually do not need to. The built-in mix phx.gen.auth generator scaffolds a complete authentication system, complete with magic link sign-in, optional password login, email confirmation, session management, and scopes for secure data access.

This chapter walks you through generating that system, running the tests, and seeing the sent emails in your browser during development (no external SMTP server needed).

Running the generator

Create a fresh Phoenix app that includes Ecto (authentication needs a database):

$ mix phx.new auth_demo
$ cd auth_demo
$ mix ecto.create

Then run the phx.gen.auth generator. Accounts is the name of the context, User the schema, and users the table:

$ mix phx.gen.auth Accounts User users (1)
[...]
* creating priv/repo/migrations/...create_users_auth_tables.exs
* creating lib/auth_demo/accounts/user_notifier.ex
* creating lib/auth_demo/accounts/user.ex
* creating lib/auth_demo/accounts/user_token.ex
* creating lib/auth_demo/accounts.ex
* creating lib/auth_demo_web/user_auth.ex
* creating lib/auth_demo_web/controllers/user_session_controller.ex
[...]
1 Besides the schema, context, and migrations, the generator adds a full set of LiveView-powered registration, login, and settings pages, plus a user_auth.ex plug that enforces authentication.

Phoenix 1.8’s default flow is magic-link: users enter their email, get a single-use link, and log in by clicking it. Password login is opt-in and can be enabled by the user from the settings screen.

Finish the install:

$ mix deps.get
$ mix ecto.migrate
$ mix test (1)
$ mix phx.server (2)
1 The generator ships with a comprehensive test suite. All tests should pass out of the box.
2 Start the server and open http://localhost:4000. You will see Log in and Register links in the page header.
Agentic Coding Tip: Don’t Let the Agent Roll Its Own Auth

Authentication is the one topic where you most need the agent to keep its hands in its pockets. When asked to "add a login system," Claude can, and sometimes will, reach for one of these wrong turns:

  • Implementing password hashing with :crypto.hash(:sha256, …​) or a hand-rolled BCrypt call instead of the bcrypt_elixir wrapper the generator wires up.

  • Writing a magic-link or password-reset flow with random tokens stored in plaintext, instead of the signed, single-use UserToken records the generator produces.

  • Storing the logged-in user as session["user_id"] and trusting it, instead of the revocable user_token mechanism the generator’s UserAuth plug uses.

  • Reinventing the require_authenticated_user plug as a before_action callback or a custom router pipeline.

Each of these is a quiet foot-gun: the code works in development, passes a simple test, and has a flaw (timing attack, unsalted hash, session fixation, stolen-token reuse, no single-use enforcement) that will never show up in normal use.

Rule to add to your project’s CLAUDE.md:

For authentication, always use `mix phx.gen.auth` as the
starting point. Build on top of what the generator
produces: `bcrypt_elixir` for hashing, the `UserToken`
schema for magic links and password resets, the
`UserAuth` plug for session enforcement, scopes for
per-user data access. Never hand-roll password hashing,
token generation, session storage, or the auth plug.
If a requirement seems to need custom crypto, stop and
let me make the call before writing any code.

Anything beyond email+password (OAuth, 2FA, SAML, passkeys) belongs in a dedicated package (ueberauth, ex_webauthn, a commercial IdP integration), not in a hand-written flow. Treat "write me a 2FA implementation from scratch" as a pause-and-confirm moment, not a task to hand off.

Watching emails land locally

The generator uses Swoosh for mail, which is bundled with Phoenix. In development Swoosh uses the Swoosh.Adapters.Local adapter, so nothing is sent over the network. Instead, emails are available on the Swoosh mailbox route that Phoenix 1.8 mounts by default:

http://localhost:4000/dev/mailbox

Sign up a new user through the UI, then open /dev/mailbox in the browser. You will see the confirmation email with the magic link. Click it, and you are logged in.

The mailbox route is only enabled in development (Application.compile_env(:auth_demo, :dev_routes)). Production emails require a real adapter such as Swoosh.Adapters.SMTP, Swoosh.Adapters.Postmark, or Swoosh.Adapters.Sendgrid. The Swoosh docs list them all.

Scopes: secure data access by default

Phoenix 1.8 introduces scopes together with phx.gen.auth. A scope is a struct that travels through your controllers and LiveViews and carries the current user. The generator produces functions such as Accounts.list_notes(scope) instead of Accounts.list_notes(user). That shape makes it natural to add tenant-based access, role checks, or multi-scope composition later without retrofitting the code.

You will see the scope referenced throughout the generated module:

defmodule AuthDemoWeb.UserAuth do
  def require_authenticated_user(conn, _opts) do
    case conn.assigns.current_scope do (1)
      %{user: %User{}} -> conn
      _ -> redirect(conn, to: ~p"/users/log_in")
    end
  end
end
1 The scope is available in both controllers and LiveViews via @current_scope (template) or conn.assigns.current_scope (controller).

Swapping to a custom mail adapter (production)

For production you usually want a real sending backend. Edit config/runtime.exs:

if config_env() == :prod do
  config :auth_demo, AuthDemo.Mailer,
    adapter: Swoosh.Adapters.SMTP,
    relay: System.fetch_env!("SMTP_HOST"),
    username: System.fetch_env!("SMTP_USER"),
    password: System.fetch_env!("SMTP_PASS"),
    ssl: true,
    port: 465
end

No code changes are required in user_notifier.ex: the same Elixir code works with any Swoosh adapter.

Next steps

The generated code is a solid starting point and intentionally understandable. When you need more (OAuth, SSO, 2FA, API tokens), the simplest path is usually to extend what the generator produced rather than swap in a whole new library. The official mix phx.gen.auth documentation lists every flag, including --hashing-lib argon2 for teams that prefer Argon2 over the default bcrypt.