What a backend implements: who someone is, and their sessions. Nothing
else. The plugs, hooks, steps, paths and the revocation push are
Macaw.Auth's, the same for every backend, so a backend never
reimplements them. And one check: check_config!/0, which the
application calls at start, so a misconfigured backend refuses boot.
Every function but check_config!/0 takes opts; :now (a
DateTime) overrides the clock, so expiry is testable. A backend never
raises for an ordinary refusal: it returns an error value. Only
check_config!/0 raises, for a fault in its configuration. It must:
- store no more than a hash of a session token;
- spend the same time refusing an unknown user as a wrong password, and give both the same refusal;
- refuse a disabled user's sessions, and expire idle ones.
The conformance kit (Macaw.Auth.Conformance) checks what it can of
these: no token stored as it is, the same refusal, disabled and idle
sessions refused. It does not check timing; a backend's own suite does.
Summary
Callbacks
Checks credentials (%{username: _, password: _} at least). context
carries :remote_ip, for throttling by address.
Checks the backend's whole configuration, as its first sign-in would
read it: :ok, or a raise naming the fault (the setting, and what is
wrong with it). Macaw.Auth.Application calls it at start for the
configured backend, and matches its :ok, so a fault refuses boot
rather than failing a sign-in.
Records that a session has completed a step (:second_factor,
:password_change): :ok, or {:error, :invalid} when the session
doesn't exist (revoked meanwhile), never a raise.
Starts a session for a user who passed authenticate/2; returns the
token for the client, or {:error, :invalid} when the user no longer
exists (deleted since signing in), never a session for them. Options:
:remote_ip, :user_agent, :second_factor_passed.
Resolves a token. touch: true (a request) may record use, no more than
the backend's interval; touch: false (a re-check) never extends a
session.
As fetch_session/2, by session id: what an open page re-checks with.
A user's sessions, newest first.
Revokes sessions: one id, or every session of a user ({:user, id}),
with :except keeping one. Returns {user_id, revoked_ids}, the ids
empty when there was nothing to revoke.
How long a session may go unused before it expires, in seconds: the
idle limit fetch_session/2 expires sessions by. The remember cookie
lives this long (Macaw.Auth.Plug.log_in/3).
Checks a second-factor code for the user of a session (a TOTP code or a
recovery code); on success completes the session's :second_factor
step. A code is accepted once: the same code again is refused. A
backend throttles these attempts as it does sign-ins. :ok, or
{:error, :invalid_code | :throttled}, or the session's own refusal.
Options: :remote_ip, :now.
Types
Callbacks
@callback authenticate(credentials(), opts()) :: {:ok, Macaw.Auth.User.t()} | {:error, refusal()}
Checks credentials (%{username: _, password: _} at least). context
carries :remote_ip, for throttling by address.
@callback check_config!() :: :ok
Checks the backend's whole configuration, as its first sign-in would
read it: :ok, or a raise naming the fault (the setting, and what is
wrong with it). Macaw.Auth.Application calls it at start for the
configured backend, and matches its :ok, so a fault refuses boot
rather than failing a sign-in.
@callback complete_step(term(), Macaw.Auth.step(), opts()) :: :ok | {:error, :invalid}
Records that a session has completed a step (:second_factor,
:password_change): :ok, or {:error, :invalid} when the session
doesn't exist (revoked meanwhile), never a raise.
@callback create_session(Macaw.Auth.User.t(), opts()) :: {:ok, binary(), Macaw.Auth.Session.t()} | {:error, :invalid}
Starts a session for a user who passed authenticate/2; returns the
token for the client, or {:error, :invalid} when the user no longer
exists (deleted since signing in), never a session for them. Options:
:remote_ip, :user_agent, :second_factor_passed.
@callback fetch_session(binary(), opts()) :: {:ok, Macaw.Auth.User.t(), Macaw.Auth.Session.t()} | {:error, :invalid | :expired | :disabled}
Resolves a token. touch: true (a request) may record use, no more than
the backend's interval; touch: false (a re-check) never extends a
session.
@callback fetch_session_by_id(term(), opts()) :: {:ok, Macaw.Auth.User.t(), Macaw.Auth.Session.t()} | {:error, :invalid | :expired | :disabled}
As fetch_session/2, by session id: what an open page re-checks with.
@callback list_sessions(term(), opts()) :: [Macaw.Auth.Session.t()]
A user's sessions, newest first.
Revokes sessions: one id, or every session of a user ({:user, id}),
with :except keeping one. Returns {user_id, revoked_ids}, the ids
empty when there was nothing to revoke.
@callback session_idle_seconds(opts()) :: pos_integer()
How long a session may go unused before it expires, in seconds: the
idle limit fetch_session/2 expires sessions by. The remember cookie
lives this long (Macaw.Auth.Plug.log_in/3).
@callback verify_second_factor(term(), term(), opts()) :: :ok | {:error, :invalid_code | :throttled | :invalid | :expired | :disabled}
Checks a second-factor code for the user of a session (a TOTP code or a
recovery code); on success completes the session's :second_factor
step. A code is accepted once: the same code again is refused. A
backend throttles these attempts as it does sign-ins. :ok, or
{:error, :invalid_code | :throttled}, or the session's own refusal.
Options: :remote_ip, :now.