Macaw.Auth.Ash.Sessions (macaw_auth_ash v0.3.1)

Copy Markdown View Source

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; a jti alone 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 own revoked? 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_days expires 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's token_lifetime) is the session's absolute limit, at least idle_days (Macaw.Auth.Ash.Config refuses a shorter one).
  • A deleted user has no new session. create/2 for 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

complete_step(id, step, opts)

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

create(user, opts)

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

fetch(token, opts)

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

fetch_by_id(id, opts)

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

list(user_id, opts)

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

A user's sessions, newest first (created at, then id: timestamps can tie).

revoke(id, opts)

@spec revoke(term(), keyword()) :: {term(), [term()]}

Revokes one session (an id) or a user's ({:user, id}, keeping :except): {user_id, ids}, the ids empty when nothing was there.

subject_user_id(subject, user_resource)

@spec subject_user_id(String.t(), module()) :: {:ok, String.t()} | :error

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.