Authentication for Phoenix hosts: one contract, with a backend behind it.
An app configures a backend and its paths:
config :macaw_auth,
backend: Macaw.Auth.Native,
paths: %{sign_in: "/login", sign_out: "/logout", home: "/",
second_factor: "/login/totp", second_factor_enroll: "/totp/enroll",
password_change: "/password"},
pubsub: MyApp.PubSub,
endpoint: MyAppWeb.Endpoint,
authorizer: MyApp.Authorizerand uses the plugs (Macaw.Auth.Plug) and LiveView hooks
(Macaw.Auth.Live). Everything here is the same for every backend:
Steps. A session may owe steps before the app is open to it, in order: a second factor (an enrolled user), enrolling one (policy requires it), a password change. Meanwhile only that step's page and sign-out are reachable.
Revocation reaches open pages. Every revocation goes through
revoke/2, which tells each open LiveView of the user's sessions, so a revoked page disconnects at once. Without:pubsubthat is this node (aRegistry); with it, every node. With:endpoint(the host's Phoenix endpoint), it also broadcasts"disconnect"on each revoked session'slive_socket_id/1, whichMacaw.Auth.Plug.log_in/3puts in the session, so Phoenix drops those sockets too:config :macaw_auth, endpoint: MyAppWeb.EndpointThe viewer.
viewer/1gives a Macaw page the viewer: identity and roles, never preferences.What a user may do is the app's.
authorize/3asks the app'sMacaw.Auth.Authorizer; with none configured, nothing is allowed.
Summary
Functions
The paths reachable while a session owes step.
Tells user_id's open pages that sessions ids ended, so they disconnect.
Checks credentials through the backend.
Whether user may do action to resource: the answer of the app's
authorizer (config :macaw_auth, authorizer: MyApp.Authorizer, a
Macaw.Auth.Authorizer), and false when none is configured. An answer
other than true or false raises CaseClauseError, and whatever the
authorizer raises goes on up: a fault in the app is never a refusal.
The configured backend module.
Records a completed step on a session: :ok, or {:error, :invalid}
when the session no longer exists (revoked meanwhile).
Starts a session. The second factor counts as passed unless the user has
one enrolled or required, which the session then owes. {:error, :invalid} when the backend refuses it (the user no longer exists).
Resolves a token: {:ok, user, session} or {:error, reason}.
Resolves a session by id (an open page's re-check).
A user's sessions, newest first.
The live_socket_id of session session_id,
"macaw_auth_sessions:<session id>": Macaw.Auth.Plug.log_in/3 puts it
in the session, so Phoenix names that session's LiveView sockets by it,
and a revocation broadcasts "disconnect" on it (announce_revoked/2).
A configured path (:sign_in, :home, a step), else its default. A
name neither configured nor one of the defaults raises ArgumentError
naming it.
The step a session owes before the app, or nil: a second factor (an enrolled user who hasn't passed it this session), enrolling one (policy requires it), then a password change.
Revokes one session (an id) or all of a user's ({:user, id}, with
:except), and tells their open pages, which disconnect.
How long a session may go unused before the backend expires it, in
seconds: the remember cookie's max_age.
The steps, in the order a session owes them.
Subscribes the caller to revocations of user_id's sessions.
Checks a second-factor code for session session_id's user; on success
the session's :second_factor step is complete. Options: :remote_ip
(for the throttle), :now.
The viewer for a Macaw page: %{id, username, roles, source}, or nil when
signed out. Identity and access only; display preferences are state,
never part of the viewer (macaw_compose spec 01 §2).
Types
Functions
The paths reachable while a session owes step.
Tells user_id's open pages that sessions ids ended, so they disconnect.
revoke/2 calls this after its backend revokes. A backend whose sessions
can also be revoked outside macaw_auth (an Ash token revoked through Ash,
say) calls it from that path too, so open pages hear of every revocation,
whoever made it. Announcing nothing is a no-op.
With config :macaw_auth, endpoint: MyAppWeb.Endpoint, it also
broadcasts "disconnect" on each session's live_socket_id/1, so
Phoenix closes those sessions' LiveView sockets as well. Without
:endpoint that broadcast is skipped; the hooks' guard
(Macaw.Auth.Live) still takes the open pages away.
@spec authenticate(map(), keyword()) :: {:ok, Macaw.Auth.User.t()} | {:error, term()}
Checks credentials through the backend.
@spec authorize(Macaw.Auth.User.t(), atom(), term()) :: boolean()
Whether user may do action to resource: the answer of the app's
authorizer (config :macaw_auth, authorizer: MyApp.Authorizer, a
Macaw.Auth.Authorizer), and false when none is configured. An answer
other than true or false raises CaseClauseError, and whatever the
authorizer raises goes on up: a fault in the app is never a refusal.
@spec backend() :: module()
The configured backend module.
Records a completed step on a session: :ok, or {:error, :invalid}
when the session no longer exists (revoked meanwhile).
@spec create_session(Macaw.Auth.User.t(), keyword()) :: {:ok, binary(), Macaw.Auth.Session.t()} | {:error, :invalid}
Starts a session. The second factor counts as passed unless the user has
one enrolled or required, which the session then owes. {:error, :invalid} when the backend refuses it (the user no longer exists).
@spec fetch(binary() | nil, keyword()) :: {:ok, Macaw.Auth.User.t(), Macaw.Auth.Session.t()} | {:error, atom()}
Resolves a token: {:ok, user, session} or {:error, reason}.
@spec fetch_by_id(term(), keyword()) :: {:ok, Macaw.Auth.User.t(), Macaw.Auth.Session.t()} | {:error, atom()}
Resolves a session by id (an open page's re-check).
@spec list_sessions(term(), keyword()) :: [Macaw.Auth.Session.t()]
A user's sessions, newest first.
The live_socket_id of session session_id,
"macaw_auth_sessions:<session id>": Macaw.Auth.Plug.log_in/3 puts it
in the session, so Phoenix names that session's LiveView sockets by it,
and a revocation broadcasts "disconnect" on it (announce_revoked/2).
A configured path (:sign_in, :home, a step), else its default. A
name neither configured nor one of the defaults raises ArgumentError
naming it.
@spec pending_step(Macaw.Auth.User.t(), Macaw.Auth.Session.t()) :: step() | nil
The step a session owes before the app, or nil: a second factor (an enrolled user who hasn't passed it this session), enrolling one (policy requires it), then a password change.
Revokes one session (an id) or all of a user's ({:user, id}, with
:except), and tells their open pages, which disconnect.
@spec session_idle_seconds(keyword()) :: pos_integer()
How long a session may go unused before the backend expires it, in
seconds: the remember cookie's max_age.
@spec steps() :: [step()]
The steps, in the order a session owes them.
@spec subscribe(term()) :: :ok
Subscribes the caller to revocations of user_id's sessions.
Checks a second-factor code for session session_id's user; on success
the session's :second_factor step is complete. Options: :remote_ip
(for the throttle), :now.
@spec viewer(Plug.Conn.t() | map()) :: map() | nil
The viewer for a Macaw page: %{id, username, roles, source}, or nil when
signed out. Identity and access only; display preferences are state,
never part of the viewer (macaw_compose spec 01 §2).