Guide: Customizing PhxAuthPlus

View Source

This guide covers common customizations after generating authentication with mix phx_auth_plus.gen.auth.

Table of Contents


Adding Custom Fields to User

The generated User schema has email, hashed_password, and confirmed_at. To add custom fields (e.g., first_name, role):

1. Add fields to the schema

# lib/my_app/user.ex
schema "users" do
  field :email, :string
  field :first_name, :string
  field :last_name, :string
  field :role, :string, default: "user"
  field :password, :string, virtual: true, redact: true
  field :hashed_password, :string, redact: true
  field :current_password, :string, virtual: true, redact: true
  field :confirmed_at, :naive_datetime
  timestamps(type: :naive_datetime)
end

2. Add fields to the registration changeset

# lib/my_app/user.ex
def registration_changeset(user, attrs, opts \\ []) do
  user
  |> cast(attrs, [:email, :password, :first_name, :last_name])
  |> validate_required([:email, :password, :first_name, :last_name])
  |> validate_email()
  |> validate_password(opts)
  |> maybe_hash_password(opts)
end

3. Add fields to the migration

# priv/repo/migrations/*_create_users_auth_tables.exs
create table(:users) do
  add :email, :string, null: false
  add :first_name, :string
  add :last_name, :string
  add :role, :string, default: "user"
  # hashed_password is nullable to support magic link users without a password
  add :hashed_password, :string
  add :confirmed_at, :naive_datetime
  timestamps(type: :naive_datetime)
end

4. Add fields to the registration form

<!-- lib/my_app_web/user_registration_live.ex -->
<.input field={@form[:first_name]} type="text" label="First name" />
<.input field={@form[:last_name]} type="text" label="Last name" />

Role-Based Authorization

1. Add a role field (see above)

2. Create an authorization module

# lib/my_app/accounts/authorization.ex
defmodule MyApp.Accounts.Authorization do
  alias MyApp.User

  def admin?(%User{role: "admin"}), do: true
  def admin?(_), do: false

  def can?(%User{role: "admin"}, _), do: true
  def can?(%User{role: "user"}, :read), do: true
  def can?(_, :read), do: true
  def can?(_, _), do: false
end

3. Create a require_admin plug

# lib/my_app_web/user_auth.ex
def require_admin_user(conn, _opts) do
  if conn.assigns.current_scope && conn.assigns.current_scope.user.role == "admin" do
    conn
  else
    conn
    |> put_flash(:error, "You must be an admin to access this page.")
    |> redirect(to: ~p"/")
    |> halt()
  end
end

4. Use in router

pipeline :admin do
  plug :require_authenticated_user
  plug :require_admin_user
end

scope "/admin", MyAppWeb do
  pipe_through [:browser, :admin]
  # admin routes here
end

Configuring the Mailer

The generated UserNotifier uses Swoosh to send emails. By default in dev, emails are captured by the local adapter and visible at /dev/mailbox.

Production with SMTP

# config/runtime.exs
if config_env() == :prod do
  config :my_app, MyApp.Mailer,
    adapter: Swoosh.Adapters.SMTP,
    relay: "smtp.example.com",
    username: "noreply@example.com",
    password: System.get_env("SMTP_PASSWORD"),
    port: 587,
    tls: :always
end

Other adapters

Swoosh supports many providers: SendGrid, Mailgun, Postmark, SES, etc. See Swoosh adapters.


Changing Password Requirements

The generated password_changeset validates:

  • Minimum 12 characters
  • Maximum 72 characters (bcrypt limit)
  • At least one lowercase, one uppercase, and one special character or number

These are commented out by default (as in Phoenix 1.8.9). To enforce them:

# lib/my_app/user.ex
defp validate_password(changeset, opts) do
  changeset
  |> validate_required([:password])
  |> validate_length(:password, min: 12, max: 72)
  # Uncomment to enforce complexity:
  |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
  |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
  |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
  |> maybe_hash_password(opts)
end

Disabling Account Confirmation

If you don't want email confirmation:

  1. Remove the confirmed_at field from the schema and migration
  2. Remove the confirmation routes from the router
  3. Remove confirm_user/1 and deliver_user_confirmation_instructions/2 from Accounts
  4. Remove UserConfirmationLive
  5. Remove the :confirm token context from UserToken

Or simply: generate with confirmation, then remove what you don't need. The confirmation flow is isolated enough to remove cleanly.


Using API Tokens

PhxAuthPlus generates session-based auth for browsers. For API tokens, you can extend the UserToken schema:

# lib/my_app/user_token.ex
def build_api_token(user) do
  {token, user_token} = UserToken.build_hashed_token(user, "api")
  Repo.insert!(user_token)
  token
end

def verify_api_token_query(token) do
  case UserToken.verify_hashed_token_query(token, "api") do
    {:ok, query} -> {:ok, query}
    :error -> :error
  end
end
# lib/my_app_web/api_auth.ex
def authenticate_api(conn, _opts) do
  with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
       {:ok, user} <- Accounts.get_user_by_api_token(token) do
    assign(conn, :current_scope, %MyApp.Scope{user: user})
  else
    _ -> conn |> put_status(:unauthorized) |> json(%{error: "unauthorized"}) |> halt()
  end
end

Switching Hashing Libraries

You can switch at generation time:

mix phx_auth_plus.gen.auth Accounts User users --hashing-lib argon2

To switch after generation, change the hashing module calls in:

  • lib/my_app/user.ex (hash_pwd_salt, verify_pass, no_user_verify)
  • Update the dependency in mix.exs

Umbrella Project Support

For umbrella projects, use --context-app:

mix phx_auth_plus.gen.auth Accounts User users --context-app my_app_core

This generates the context and schemas in my_app_core and the web files in the current web app.