# `Macaw.Auth`
[🔗](https://github.com/dcoai/macaw_auth/blob/v0.4.1/lib/macaw/auth.ex#L1)

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.

# `step`

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

# `allowed_paths`

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

The paths reachable while a session owes `step`.

# `announce_revoked`

```elixir
@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`

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

Checks credentials through the backend.

# `authorize`

```elixir
@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`

```elixir
@spec backend() :: module()
```

The configured backend module.

# `complete_step`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

A user's sessions, newest first.

# `live_socket_id`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

The steps, in the order a session owes them.

# `subscribe`

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

Subscribes the caller to revocations of `user_id`'s sessions.

# `verify_second_factor`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
