Macaw.Auth.Ash's sessions, kept in the host's AshAuthentication token
resource.
A session is a stored user token. create/2 signs an ordinary
AshAuthentication user JWT for the user (AshAuthentication.Jwt, the
resource's own secret, lifetime and claims) and stores its row: the
jti, purpose "user", subject and expiry, and Macaw's metadata in
extra_data["macaw_auth"] (when it was made and last used, the client's
address and agent, the second factor, the steps done). The JWT goes to
the client; the session's id is its jti.
Why the token resource rather than a session table of our own: it is
where an Ash host already keeps tokens, so one revocation means the same
to AshAuthentication's plugs and to Macaw (a revoked session is a token
AshAuthentication refuses too, and a token AshAuthentication revokes is a
session Macaw refuses), its expunger clears the rows, and the host has no
second table to migrate. What it lacked, an update of extra_data and a
revocation by jti that keeps the row's expiry, Macaw.Auth.Ash.TokenResource
adds.
The contract's guarantees, and how:
- No token stored. The row holds the
jti, a random value inside the JWT's signed payload, never the JWT; ajtialone signs nobody in, since a token must carry the resource's signature. The kit checks every stored value against the tokens it was given. - Revocation. Revoking sets the row's purpose to
"revocation"(:macaw_auth_revoke), the state AshAuthentication's ownrevoked?check looks for; the row keeps its expiry, so the expunger removes it when the JWT could no longer verify anyway. A session is valid only while its row has purpose"user"and Macaw's metadata, so a token AshAuthentication minted for itself is not a Macaw session. - Idle expiry slides. A session unused for
idle_daysexpires and is revoked at its next look-up; a request (touch: true) records use at most once an hour, and a re-check (touch: false) never does. The JWT's own expiry (the user resource'stoken_lifetime) is the session's absolute limit, at leastidle_days(Macaw.Auth.Ash.Configrefuses a shorter one). - A deleted user has no new session.
create/2for a user deleted after signing in is{:error, :invalid}. - A disabled user has no session. Looking one up revokes it.
Every function honors :now. Every read and write is made with
authorize?: false: this is the authentication layer itself, before
there is an actor to authorize.
Summary
Functions
Records a step on a session: the second factor, or another step done.
:ok, or {:error, :invalid} when there is no live session by that id
(revoked, by Macaw or AshAuthentication, even between this read and its
write; or never one), which is left as it is.
Starts a session for user: {:ok, token, session}, the JWT for the
client only, or {:error, :invalid} when the user is gone (deleted
after signing in): there is no record to sign a token for, and nothing
is stored. Options: :remote_ip, :user_agent,
:second_factor_passed (default true), :now.
Resolves a token: its signature and claims verified against the user
resource, then its session. {:ok, user, session} or {:error, :invalid | :expired | :disabled}. Options: :touch (default false), :now.
As fetch/2, by session id (the token's jti): what an open page re-checks with.
A user's sessions, newest first (created at, then id: timestamps can tie).
Revokes one session (an id) or a user's ({:user, id}, keeping
:except): {user_id, ids}, the ids empty when nothing was there.
The user id a token's subject names (the primary key's value, as the
subject carries it), or :error for another resource's subject.
Functions
@spec complete_step(term(), Macaw.Auth.step(), keyword()) :: :ok | {:error, :invalid}
Records a step on a session: the second factor, or another step done.
:ok, or {:error, :invalid} when there is no live session by that id
(revoked, by Macaw or AshAuthentication, even between this read and its
write; or never one), which is left as it is.
@spec create(Macaw.Auth.User.t(), keyword()) :: {:ok, binary(), Macaw.Auth.Session.t()} | {:error, :invalid}
Starts a session for user: {:ok, token, session}, the JWT for the
client only, or {:error, :invalid} when the user is gone (deleted
after signing in): there is no record to sign a token for, and nothing
is stored. Options: :remote_ip, :user_agent,
:second_factor_passed (default true), :now.
@spec fetch(binary(), keyword()) :: {:ok, Macaw.Auth.User.t(), Macaw.Auth.Session.t()} | {:error, :invalid | :expired | :disabled}
Resolves a token: its signature and claims verified against the user
resource, then its session. {:ok, user, session} or {:error, :invalid | :expired | :disabled}. Options: :touch (default false), :now.
@spec fetch_by_id(term(), keyword()) :: {:ok, Macaw.Auth.User.t(), Macaw.Auth.Session.t()} | {:error, :invalid | :expired | :disabled}
As fetch/2, by session id (the token's jti): what an open page re-checks with.
@spec list(term(), keyword()) :: [Macaw.Auth.Session.t()]
A user's sessions, newest first (created at, then id: timestamps can tie).
Revokes one session (an id) or a user's ({:user, id}, keeping
:except): {user_id, ids}, the ids empty when nothing was there.
The user id a token's subject names (the primary key's value, as the
subject carries it), or :error for another resource's subject.