PhoenixKitWeb.Users.Auth (phoenix_kit v1.7.235)

Copy Markdown View Source

Authentication and authorization plugs for PhoenixKit user management.

This module provides plugs and functions for handling user authentication, session management, and access control in Phoenix applications using PhoenixKit.

Key Features

  • User authentication with email and password
  • Remember me functionality with secure cookies
  • Session-based authentication
  • Route protection and access control
  • Module-level permission enforcement via on_mount hooks
  • Integration with Phoenix LiveView on_mount callbacks

on_mount Hooks

  • :phoenix_kit_ensure_admin — Requires Owner/Admin role, or a custom role with at least one permission. Every role is then checked against the permission key mapped to the current admin view: Owner passes because its scope holds every key by construction; Admin holds keys as real, Owner-revocable rows (seeded/auto-granted by default). Disabled modules block everyone. Views that resolve to no permission key allow only a scope holding every enabled permission — role-agnostic, so a named Admin whose keys an Owner has partially revoked is denied too. The first mount of such a view logs a warning, whoever mounts it, so the missing mapping surfaces before a colleague hits the 403.
  • :phoenix_kit_ensure_module_access — Checks that the feature module is permitted for the scope and (for custom roles) enabled.

Usage

The plugs in this module are automatically configured when using PhoenixKitWeb.Integration.phoenix_kit_routes/0 macro in your router.

Summary

Functions

Reconstructs and assigns the current user + scope on an embedded LiveView mount from a host-supplied session["current_user_uuid"].

Whether scope may open the admin LiveView view_module — the SAME decision :phoenix_kit_ensure_admin enforces on mount, exposed as a pure boolean.

Fetches the current user and creates a scope for authentication context.

Authenticates the user by looking into the session and remember me token.

Whether view is the guaranteed landing — the one admin LiveView that :phoenix_kit_ensure_admin opens to EVERY authenticated visitor, whatever permissions they hold.

Logs the user out.

Logs out a specific user by invalidating all their session tokens and broadcasting disconnect to their LiveView sessions.

Whether signing in with a magic link is available.

Whether registering via a magic link is available.

Checks if the current user is authenticated and returns a redirect socket if so.

Handles mounting and authenticating the phoenix_kit_current_user in LiveViews.

Used for routes that require the user to not be authenticated.

Redirects invalid locale URLs to the canonical default-locale shape.

Redirects full dialect code URLs to base language URLs.

Whether the "remember me" checkbox starts checked.

Whether the site allows persistent ("remember me") sessions at all.

The params a no-UI login flow (magic link, OAuth) should pass to log_in_user/3 to follow the site's persistence policy.

Whether this request already carries a persistent remember-me cookie.

Used for routes that require the user to be an admin or owner.

Used for routes that require the user to be authenticated via scope.

Used for routes that require the user to be authenticated.

Used for routes that require the user to have module-level permission.

Used for routes that require the user to be an owner.

Validates and sets the locale for the current request.

Functions

assign_embedded_current_user(socket, session)

@spec assign_embedded_current_user(Phoenix.LiveView.Socket.t(), map()) ::
  Phoenix.LiveView.Socket.t()

Reconstructs and assigns the current user + scope on an embedded LiveView mount from a host-supplied session["current_user_uuid"].

A LiveView rendered via live_render/3 mounts with :not_mounted_at_router, so it never runs a router live_session's on_mount hook (e.g. :phoenix_kit_ensure_admin) — leaving :phoenix_kit_current_scope / :phoenix_kit_current_user absent, which blinds any user-aware embedded UI (comment composers, activity-actor attribution). The host bridges identity across the live_render process boundary by passing its own authenticated user's UUID as session["current_user_uuid"] — a string, never the %User{} struct (a signed-but-not-encrypted live_render session would expose it to the client). This helper reloads that user and assigns both :phoenix_kit_current_user and :phoenix_kit_current_scope.

  • No-op when :phoenix_kit_current_scope is already assigned (router mount — the on_mount hook ran, before mount/3). Never clobbers the canonical scope.
  • An active user → assigns it + Scope.for_user(user).
  • Absent / unknown / inactive uuid, or a transient DB error → anonymous (nil user + Scope.for_user(nil)), never raising.

This reconstructs identity (audit, comment authorship), not authorization — it performs no role check. The UUID must come from the host's trusted server-side scope, never request params; and a host embedding an admin LiveView must gate the embedding page itself (the on_mount admin gate does not run for embeds).

Generic across embeddable feature modules — phoenix_kit_projects is the reference consumer.

can_access_admin_view?(scope, view_module)

@spec can_access_admin_view?(PhoenixKit.Users.Auth.Scope.t() | nil, module()) ::
  boolean()

Whether scope may open the admin LiveView view_module — the SAME decision :phoenix_kit_ensure_admin enforces on mount, exposed as a pure boolean.

Use it to decide whether to render a link, card or nav entry pointing at an admin view. "A card is visible iff the visitor can open what it links to" then holds by construction: both the card and its destination route through this one function, so the two cannot drift.

Pure — no flash, no redirect, no logging. The on_mount hook keeps those side effects (including the one-time "unmapped admin view" warning) and asks this same decision for the verdict.

The decision, branch by branch

  1. Admin-area gateScope.can_access_admin_area?/1 must hold (Owner, Admin, or any holder of at least one permission). Every /admin page mounts through :phoenix_kit_ensure_admin, which bounces a failing scope before any per-view check — every page but the landing, which is the one deliberate difference between this function and the gate (see below). So a card must respect it too: a nil scope, and an authenticated user with no permissions at all, stop here.
  2. Personal admin views — the views in @personal_admin_views (your own notification inbox / preferences) show a user their OWN data rather than granting an administrative capability, so branch 1 is the whole gate.
  3. Mapped viewpermission_key_for_admin_view/1 resolved a key:
    • the module behind the key must be enabled (PhoenixKit.Users.Permissions.feature_enabled?/1) — a disabled module is blocked for EVERYONE, Owner included;
    • a dotted SUB-permission key is checked with Scope.can?/2 (which also requires the base key — a raw sub-key without its base is an orphan); a flat key with Scope.has_module_access?/2.
  4. Unmapped view — the key resolved to nil: allowed ONLY for a scope that holds every enabled permission (Scope.holds_all_enabled_permissions?/1). This is the fail-CLOSED branch, and it is role-agnostic: Owner passes (it holds every key by construction), so does the "*" superadmin key and any role granted the whole operator baseline; a partially-revoked Admin or a narrow custom role does not. Hiding such a card from everyone but a full-access scope is exactly right — anyone else is redirected on arrival.

What this deliberately does NOT answer: authentication, email confirmation and the account gate (require_authenticated_live/2 and live_account_gate/2 run earlier in the hook). Those bounce a visitor before any view-permission question is asked; a page deciding what to render has already passed them.

The one view where this is stricter than the mount gate

landing_view?/1 — the /admin index — is admitted by :phoenix_kit_ensure_admin for EVERY authenticated visitor, because it is the destination PhoenixKit.Utils.Routes.safe_destination/2 guarantees. This function still answers false for a scope that fails branch 1, and that is deliberate: the landing is somewhere a visitor is sent, not somewhere a menu should offer them. A permission-less visitor who lands there is greeted and shown no navigation at all (PhoenixKitWeb.Components.Dashboard.AdminSidebar.reachable_tabs/2 returns [] for exactly the same scopes), so answering true would render a link to a page they are already on, inside a shell built for operators.

Every other view answers identically here and at the gate, which is what makes "a card is visible iff its destination admits you" true — a visible card is never a redirect, and the only page that admits more than it advertises is the one no card points at.

Examples

# compute in the LiveView, keep HEEx declarative
assign(socket,
  show_users_card?:
    Auth.can_access_admin_view?(scope, PhoenixKitWeb.Live.Users.Users)
)

fetch_phoenix_kit_current_scope(conn, opts)

Fetches the current user and creates a scope for authentication context.

This plug combines user fetching with scope creation, providing a structured way to handle authentication state in your application.

The scope is assigned to :phoenix_kit_current_scope and includes both the user and authentication status.

Also verifies session fingerprints if enabled to detect session hijacking attempts.

fetch_phoenix_kit_current_user(conn, opts)

Authenticates the user by looking into the session and remember me token.

Also verifies session fingerprints if enabled to detect session hijacking attempts.

This plug is idempotent - if the user has already been fetched, it returns early to avoid duplicate database queries.

get_endpoint()

landing_view?(view)

@spec landing_view?(module() | nil) :: boolean()

Whether view is the guaranteed landing — the one admin LiveView that :phoenix_kit_ensure_admin opens to EVERY authenticated visitor, whatever permissions they hold.

That view is PhoenixKitWeb.Live.Dashboard, the /admin index. It is the terminal of PhoenixKit.Utils.Routes.safe_destination/2: the page core promises any signed-in visitor can be redirected to. A terminal that rejects its own visitor is an infinite redirect rather than a fallback, so the gate has to admit them — and the page is built for it, gating every operator block behind can_access_admin_view?/2 and showing a permission-less visitor the welcome block and nothing else.

Admission is NOT unconditional: authentication, the account gate (email confirmation, maintenance mode, blocked accounts) and the locale hook all run first, exactly as for any other admin view. Only the admin-area gate and the per-view permission check are skipped.

iex> PhoenixKitWeb.Users.Auth.landing_view?(PhoenixKitWeb.Live.Dashboard)
true

iex> PhoenixKitWeb.Users.Auth.landing_view?(PhoenixKitWeb.Live.Users.Users)
false

log_in_user(conn, user, params \\ %{})

Logs the user in.

It renews the session ID and clears the whole session to avoid fixation attacks. See the renew_session function to customize this behaviour.

It also sets a :live_socket_id key in the session, so LiveView sessions are identified and automatically disconnected on log out. The line can be safely removed if you are not using LiveView.

Session Fingerprinting

When session fingerprinting is enabled, this function captures the user's IP address and user agent to create a session fingerprint. This helps detect session hijacking attempts.

log_out_user(conn)

Logs the user out.

It clears all session data for safety. See renew_session.

log_out_user_from_all_sessions(user)

Logs out a specific user by invalidating all their session tokens and broadcasting disconnect to their LiveView sessions.

This function is useful when user roles or permissions change and you need to force re-authentication to ensure the user gets updated permissions in their session.

Parameters

  • user: The user to log out from all sessions

Examples

iex> log_out_user_from_all_sessions(user)
:ok

maybe_redirect_authenticated(socket)

Checks if the current user is authenticated and returns a redirect socket if so.

Used by auth pages (login, register, etc.) that should not be accessible to already-authenticated users when placed in a shared public live_session.

Returns {:redirect, redirected_socket} if authenticated (caller should halt), or :cont if not authenticated (caller should proceed with normal mount).

on_mount(arg1, params, session, socket)

Handles mounting and authenticating the phoenix_kit_current_user in LiveViews.

on_mount arguments

  • :phoenix_kit_mount_current_user - Assigns phoenix_kit_current_user to socket assigns based on user_token, or nil if there's no user_token or no matching user.

  • :phoenix_kit_mount_current_scope - Assigns both phoenix_kit_current_user and phoenix_kit_current_scope to socket assigns. The scope provides structured access to authentication state.

  • :phoenix_kit_ensure_authenticated - Authenticates the user from the session, and assigns the phoenix_kit_current_user to socket assigns based on user_token. Redirects to login page if there's no logged user.

  • :phoenix_kit_ensure_authenticated_scope - Authenticates the user via scope system, assigns both phoenix_kit_current_user and phoenix_kit_current_scope.

  • :phoenix_kit_ensure_owner - Ensures the user has owner role, and redirects to the home page if not.

  • :phoenix_kit_ensure_admin - Ensures the user has admin or owner role, and redirects to the home page if not. Redirects to login page if there's no logged user.

  • :phoenix_kit_redirect_if_user_is_authenticated - Authenticates the user from the session. Redirects to signed_in_path if there's a logged user.

  • :phoenix_kit_redirect_if_authenticated_scope - Checks authentication via scope system. Redirects to signed_in_path if there's a logged user.

Examples

Use the on_mount lifecycle macro in LiveViews to mount or authenticate the current_user:

defmodule PhoenixKitWeb.PageLive do
  use PhoenixKitWeb, :live_view

  on_mount {PhoenixKitWeb.Users.Auth, :phoenix_kit_mount_current_user}
  ...
end

Or use the scope system for better encapsulation:

defmodule PhoenixKitWeb.PageLive do
  use PhoenixKitWeb, :live_view

  on_mount {PhoenixKitWeb.Users.Auth, :phoenix_kit_mount_current_scope}
  ...
end

Or use the live_session of your router to invoke the on_mount callback:

live_session :authenticated, on_mount: [{PhoenixKitWeb.Users.Auth, :phoenix_kit_ensure_authenticated_scope}] do
  live "/profile", ProfileLive, :index
end

redirect_if_user_is_authenticated(conn, opts)

Used for routes that require the user to not be authenticated.

redirect_invalid_locale(conn, invalid_locale)

Redirects invalid locale URLs to the canonical default-locale shape.

Takes the current URL path and replaces the invalid locale segment so the redirect target matches the rest of the app's URL emission:

  • With default_language_no_prefix? ON → strip the segment entirely (the canonical primary shape is prefixless, e.g. /phoenix_kit/admin).
  • With the setting OFF (default) → swap the invalid segment for the primary base code so the canonical prefixed shape is preserved (e.g. /phoenix_kit/xx/admin/phoenix_kit/en/admin).

redirect_to_base_locale(conn, full_dialect)

Redirects full dialect code URLs to base language URLs.

This function handles backward compatibility by redirecting old URLs with full dialect codes (en-US, es-MX) to the new simplified base code URLs (en, es).

Sends a 302. The docs here used to promise a 301, and the call passed status: 301 — but Phoenix.Controller.redirect/2 sends conn.status || 302 and ignores an :status option, so no 301 was ever emitted. The option has been removed rather than made real: a cached permanent redirect off a dialect code is hard to walk back if the dialect set changes, and nothing here needs permanence. Use put_status(:moved_permanently) before redirect/2 if that ever changes.

Examples

iex> redirect_to_base_locale(conn, "en-US")
# /phoenix_kit/en-US/admin → /phoenix_kit/en/admin

iex> redirect_to_base_locale(conn, "es-MX")
# /phoenix_kit/es-MX/users?page=2 → /phoenix_kit/es/users?page=2

Preservation

  • Query parameters preserved (via with_query_string/2 — they were not, before: this rebuilds a path, and conn.request_path stops at the "?")
  • Request method unchanged (GET → GET)
  • Full path structure maintained

URL fragments are NOT preserved, and cannot be: a fragment never leaves the browser, so the server has nothing to copy. The client re-applies its own fragment to the Location it follows.

Notes

  • Halts conn pipeline (no further processing)
  • Logged for monitoring migration patterns

remember_me_default?()

@spec remember_me_default?() :: boolean()

Whether the "remember me" checkbox starts checked.

Site-wide default via the remember_me_default setting (default true) — users can still untick it per login. Flows with no UI to tick (magic-link login, OAuth) follow this value directly. Always false when remember_me_enabled?/0 is false.

remember_me_enabled?()

@spec remember_me_enabled?() :: boolean()

Whether the site allows persistent ("remember me") sessions at all.

Site-wide policy via the remember_me_enabled setting (default true). When false the checkbox is hidden on every auth form and no flow can write the persistent cookie — logins last only as long as the browser session.

remember_me_params()

@spec remember_me_params() :: map()

The params a no-UI login flow (magic link, OAuth) should pass to log_in_user/3 to follow the site's persistence policy.

remembered?(conn)

@spec remembered?(Plug.Conn.t()) :: boolean()

Whether this request already carries a persistent remember-me cookie.

Lets a re-login flow that has no checkbox of its own (the password-change handoff) preserve the choice the user already made, instead of silently downgrading them to a session-only login.

require_admin(conn, opts)

Used for routes that require the user to be an admin or owner.

If you want to enforce the admin requirement without redirecting to the login page, consider using :phoenix_kit_require_authenticated_scope instead.

require_authenticated_scope(conn, opts)

Used for routes that require the user to be authenticated via scope.

This function checks authentication status through the scope system, providing a more structured approach to authentication checks.

Enforces email confirmation before allowing access to the application.

require_authenticated_user(conn, opts)

Used for routes that require the user to be authenticated.

Enforces email confirmation before allowing access to the application.

require_module_access(conn, module_key)

Used for routes that require the user to have module-level permission.

require_owner(conn, opts)

Used for routes that require the user to be an owner.

If you want to enforce the owner requirement without redirecting to the login page, consider using :phoenix_kit_require_authenticated_scope instead.

require_role(conn, role_name)

validate_and_set_locale(conn, opts)

Validates and sets the locale for the current request.

This function is called as a plug in the router to validate locale codes in the URL path. It implements PhoenixKit's simplified URL architecture:

  • URLs use base language codes (en, es, fr) for simplicity
  • Full dialect codes (en-US, es-MX) are redirected to base codes (301)
  • User preferences determine which dialect variant to use for translations
  • Translation system uses full dialect codes internally

Data Flow

  1. Check if URL contains full dialect code → redirect to base
  2. Validate base code exists in predefined language list
  3. Resolve to full dialect using user preference or default mapping
  4. Set Gettext to full dialect for translations
  5. Store both base code (for URLs) and full dialect (for translations)

Examples

# Base code in URL (preferred format)
conn = validate_and_set_locale(conn, [])
# Sets: current_locale_base="en", current_locale="en-US"

# Full dialect in URL (legacy/bookmarks)
conn = validate_and_set_locale(%{path_params: %{"locale" => "en-US"}}, [])
# Redirects 301 to: /en/...

# Invalid locale in URL
conn = validate_and_set_locale(%{path_params: %{"locale" => "xx"}}, [])
# Redirects to default locale URL