Where clients, codes, pushed authorization requests, device grants, tokens and consents live. Implemented by Sake.Auth.Store.ETS and Sake.Auth.Store.Ecto.
Summary
Callbacks
Consumes a code, if and only if it was not consumed yet, atomically. The
store keeps knowing the consumed code until it expires, as the fence
put_code_tokens/3 checks.
Takes the pushed authorization request whose request_uri has this
hash, if and only if nobody took it before, atomically: of two
concurrent calls, one gets it and the other :error. The store may
forget it then; the caller checks its expiry and its client.
Marks a refresh token used now, if and only if it was unused, atomically: two concurrent calls for the same hash must not both succeed. Returns the record as it was before this call.
Records the person's decision on a pending device grant (by its
device code's hash), if and only if it is still pending, atomically:
of two concurrent decisions, one gets :ok and the other :error.
An approval sets subject_id, narrows scopes to those granted, and
sets acr and auth_time from authentication, how the person
signed in (either may be nil).
Forgets the approval for one resource, or for every resource with :all.
Records key (a digest naming a single-use JWT: a client assertion, a
JWT grant, a DPoP proof) until expires_at, in Unix seconds: :ok the
first time, :replayed while it is recorded. Atomic across every node
that shares the store: of two calls with the same key, exactly one gets
:ok.
A poll of the token endpoint for the device grant hash, by the
client client_id, at now, atomically. :error for a grant that
does not exist or is another client's; then, in this order, :used
once its tokens were issued, :denied, :expired (from
expires_at), :slow_down when the last poll was less than
interval seconds ago, :pending, and {:approved, grant} for an
approved grant, which becomes :issued: of two concurrent polls, only
one gets it. Every poll but the first three records now as
last_polled_at.
Deletes the codes, pushed authorization requests, device grants and
tokens that expired by now; the consents inserted before
before.consents whose subject, client and resource have no live
(unexpired, unrevoked) refresh token left; and the dynamically
registered clients registered before before.unused_clients that hold
no live token, no consent, no code, no pushed request and no device
grant.
Forgets the keys once/3 recorded that expired by now, in Unix seconds.
Stores the tokens issued for a consumed code (their family_id is the
code's hash), all of them or none: none, returning :revoked, when the
family was revoked since the code was consumed (a replay of the code).
A store may refuse a code it no longer knows the same way. Atomic with revoke_family/2: whichever of the
two runs second sees the other's effect, so a replay never leaves a
live pair behind.
Remembers an approval, keyed by subject, client and resource (replacing an earlier one).
Stores the tokens issued for a device grant (their family_id is its
hash), all of them or none: none, returning :revoked, when the
family was revoked since the grant was issued (a replay of the device
code). Atomic with revoke_family/2, as put_code_tokens/3 is.
Stores the tokens a refresh rotated into, all of them or none: none,
returning :revoked, when the refresh token they replace (by hash) is
revoked or gone, as a concurrent reuse of it revokes its family.
Atomic with revoke_family/2: whichever of the two runs second sees
the other's effect.
Revokes every token of a family, and the code or device grant whose
hash is the family's id, so that put_code_tokens/3 and
put_device_tokens/3 refuse the tokens not stored yet.
Types
@type state() :: term()
Callbacks
@callback consume_code(state(), String.t()) :: {:ok, Sake.Auth.Code.t()} | :error
Consumes a code, if and only if it was not consumed yet, atomically. The
store keeps knowing the consumed code until it expires, as the fence
put_code_tokens/3 checks.
@callback consume_pushed_request(state(), hash :: String.t()) :: {:ok, Sake.Auth.PushedRequest.t()} | :error
Takes the pushed authorization request whose request_uri has this
hash, if and only if nobody took it before, atomically: of two
concurrent calls, one gets it and the other :error. The store may
forget it then; the caller checks its expiry and its client.
@callback consume_refresh(state(), String.t()) :: {:ok, Sake.Auth.TokenRecord.t()} | {:error, :used, Sake.Auth.TokenRecord.t()} | :error
Marks a refresh token used now, if and only if it was unused, atomically: two concurrent calls for the same hash must not both succeed. Returns the record as it was before this call.
@callback decide_device_grant( state(), hash :: String.t(), {:approved, subject_id :: String.t(), scopes :: [String.t()], authentication :: %{acr: String.t() | nil, auth_time: DateTime.t() | nil}} | :denied ) :: :ok | :error
Records the person's decision on a pending device grant (by its
device code's hash), if and only if it is still pending, atomically:
of two concurrent decisions, one gets :ok and the other :error.
An approval sets subject_id, narrows scopes to those granted, and
sets acr and auth_time from authentication, how the person
signed in (either may be nil).
@callback delete_consent( state(), subject_id :: String.t(), client_id :: String.t(), resource :: String.t() | :all ) :: :ok
Forgets the approval for one resource, or for every resource with :all.
@callback get_client(state(), String.t()) :: {:ok, Sake.Auth.Client.t()} | :error
@callback get_consent( state(), subject_id :: String.t(), client_id :: String.t(), resource :: String.t() ) :: {:ok, Sake.Auth.Consent.t()} | :error
@callback get_device_grant_by_user_code(state(), user_code_hash :: String.t()) :: {:ok, Sake.Auth.DeviceGrant.t()} | :error
@callback get_token(state(), :access | :refresh, String.t()) :: {:ok, Sake.Auth.TokenRecord.t()} | :error
@callback list_clients(state()) :: [Sake.Auth.Client.t()]
Records key (a digest naming a single-use JWT: a client assertion, a
JWT grant, a DPoP proof) until expires_at, in Unix seconds: :ok the
first time, :replayed while it is recorded. Atomic across every node
that shares the store: of two calls with the same key, exactly one gets
:ok.
@callback poll_device_grant( state(), hash :: String.t(), client_id :: String.t(), now :: DateTime.t() ) :: {:approved, Sake.Auth.DeviceGrant.t()} | :pending | :slow_down | :denied | :expired | :used | :error
A poll of the token endpoint for the device grant hash, by the
client client_id, at now, atomically. :error for a grant that
does not exist or is another client's; then, in this order, :used
once its tokens were issued, :denied, :expired (from
expires_at), :slow_down when the last poll was less than
interval seconds ago, :pending, and {:approved, grant} for an
approved grant, which becomes :issued: of two concurrent polls, only
one gets it. Every poll but the first three records now as
last_polled_at.
@callback prune( state(), now :: DateTime.t(), before :: %{consents: DateTime.t(), unused_clients: DateTime.t()} ) :: :ok
Deletes the codes, pushed authorization requests, device grants and
tokens that expired by now; the consents inserted before
before.consents whose subject, client and resource have no live
(unexpired, unrevoked) refresh token left; and the dynamically
registered clients registered before before.unused_clients that hold
no live token, no consent, no code, no pushed request and no device
grant.
Forgets the keys once/3 recorded that expired by now, in Unix seconds.
@callback put_client(state(), Sake.Auth.Client.t()) :: :ok
@callback put_code(state(), Sake.Auth.Code.t()) :: :ok
@callback put_code_tokens(state(), code_hash :: String.t(), [Sake.Auth.TokenRecord.t()]) :: :ok | :revoked
Stores the tokens issued for a consumed code (their family_id is the
code's hash), all of them or none: none, returning :revoked, when the
family was revoked since the code was consumed (a replay of the code).
A store may refuse a code it no longer knows the same way. Atomic with revoke_family/2: whichever of the
two runs second sees the other's effect, so a replay never leaves a
live pair behind.
@callback put_consent(state(), Sake.Auth.Consent.t()) :: :ok
Remembers an approval, keyed by subject, client and resource (replacing an earlier one).
@callback put_device_grant(state(), Sake.Auth.DeviceGrant.t()) :: :ok
@callback put_device_tokens(state(), hash :: String.t(), [Sake.Auth.TokenRecord.t()]) :: :ok | :revoked
Stores the tokens issued for a device grant (their family_id is its
hash), all of them or none: none, returning :revoked, when the
family was revoked since the grant was issued (a replay of the device
code). Atomic with revoke_family/2, as put_code_tokens/3 is.
@callback put_pushed_request(state(), Sake.Auth.PushedRequest.t()) :: :ok
@callback put_rotated_tokens(state(), refresh_hash :: String.t(), [ Sake.Auth.TokenRecord.t() ]) :: :ok | :revoked
Stores the tokens a refresh rotated into, all of them or none: none,
returning :revoked, when the refresh token they replace (by hash) is
revoked or gone, as a concurrent reuse of it revokes its family.
Atomic with revoke_family/2: whichever of the two runs second sees
the other's effect.
@callback put_token(state(), Sake.Auth.TokenRecord.t()) :: :ok
Revokes every token of a family, and the code or device grant whose
hash is the family's id, so that put_code_tokens/3 and
put_device_tokens/3 refuse the tokens not stored yet.
@callback tokens_for_subject(state(), String.t()) :: [Sake.Auth.TokenRecord.t()]