Macaw.Auth (macaw_auth v0.4.1)

Copy Markdown View Source

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.Authorizer

and 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 :pubsub that is this node (a Registry); with it, every node. With :endpoint (the host's Phoenix endpoint), it also broadcasts "disconnect" on each revoked session's live_socket_id/1, which Macaw.Auth.Plug.log_in/3 puts in the session, so Phoenix drops those sockets too:

    config :macaw_auth, endpoint: MyAppWeb.Endpoint
  • The viewer. viewer/1 gives a Macaw page the viewer: identity and roles, never preferences.

  • What a user may do is the app's. authorize/3 asks the app's Macaw.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

step()

@type step() :: :second_factor | :second_factor_enroll | :password_change

Functions

allowed_paths(step)

@spec allowed_paths(step()) :: [String.t()]

The paths reachable while a session owes step.

announce_revoked(user_id, ids)

@spec announce_revoked(term(), [term()]) :: :ok

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.

authenticate(credentials, opts \\ [])

@spec authenticate(map(), keyword()) :: {:ok, Macaw.Auth.User.t()} | {:error, term()}

Checks credentials through the backend.

authorize(user, action, resource)

@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.

backend()

@spec backend() :: module()

The configured backend module.

complete_step(session_id, step, opts \\ [])

@spec complete_step(term(), step(), keyword()) :: :ok | {:error, :invalid}

Records a completed step on a session: :ok, or {:error, :invalid} when the session no longer exists (revoked meanwhile).

create_session(user, opts \\ [])

@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).

fetch(token, opts \\ [])

@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}.

fetch_by_id(id, opts \\ [])

@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).

list_sessions(user_id, opts \\ [])

@spec list_sessions(term(), keyword()) :: [Macaw.Auth.Session.t()]

A user's sessions, newest first.

live_socket_id(session_id)

@spec live_socket_id(term()) :: String.t()

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).

path(name)

@spec path(atom()) :: String.t()

A configured path (:sign_in, :home, a step), else its default. A name neither configured nor one of the defaults raises ArgumentError naming it.

pending_step(user, session)

@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.

revoke(target, opts \\ [])

@spec revoke(term() | {:user, term()}, keyword()) :: :ok

Revokes one session (an id) or all of a user's ({:user, id}, with :except), and tells their open pages, which disconnect.

session_idle_seconds(opts \\ [])

@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.

steps()

@spec steps() :: [step()]

The steps, in the order a session owes them.

subscribe(user_id)

@spec subscribe(term()) :: :ok

Subscribes the caller to revocations of user_id's sessions.

verify_second_factor(session_id, code, opts \\ [])

@spec verify_second_factor(term(), term(), keyword()) :: :ok | {:error, atom()}

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.

viewer(arg1)

@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).