Sake.Auth.Store behaviour (Sake v0.4.3)

Copy Markdown View Source

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

state()

@type state() :: term()

Callbacks

consume_code(state, t)

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

consume_pushed_request(state, hash)

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

consume_refresh(state, t)

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

decide_device_grant(state, hash, arg3)

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

delete_client(state, t)

@callback delete_client(state(), String.t()) :: :ok

delete_consent(state, subject_id, client_id, resource)

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

get_client(state, t)

@callback get_client(state(), String.t()) :: {:ok, Sake.Auth.Client.t()} | :error

get_consent(state, subject_id, client_id, resource)

@callback get_consent(
  state(),
  subject_id :: String.t(),
  client_id :: String.t(),
  resource :: String.t()
) ::
  {:ok, Sake.Auth.Consent.t()} | :error

get_device_grant_by_user_code(state, user_code_hash)

@callback get_device_grant_by_user_code(state(), user_code_hash :: String.t()) ::
  {:ok, Sake.Auth.DeviceGrant.t()} | :error

get_token(state, arg2, t)

@callback get_token(state(), :access | :refresh, String.t()) ::
  {:ok, Sake.Auth.TokenRecord.t()} | :error

init(keyword)

@callback init(keyword()) :: state()

list_clients(state)

@callback list_clients(state()) :: [Sake.Auth.Client.t()]

once(state, key, expires_at)

@callback once(state(), key :: String.t(), expires_at :: integer()) :: :ok | :replayed

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.

poll_device_grant(state, hash, client_id, now)

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

prune(state, now, before)

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

prune_once(state, now)

@callback prune_once(state(), now :: integer()) :: :ok

Forgets the keys once/3 recorded that expired by now, in Unix seconds.

put_client(state, t)

@callback put_client(state(), Sake.Auth.Client.t()) :: :ok

put_code(state, t)

@callback put_code(state(), Sake.Auth.Code.t()) :: :ok

put_code_tokens(state, code_hash, list)

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

put_consent(state, t)

@callback put_consent(state(), Sake.Auth.Consent.t()) :: :ok

Remembers an approval, keyed by subject, client and resource (replacing an earlier one).

put_device_grant(state, t)

@callback put_device_grant(state(), Sake.Auth.DeviceGrant.t()) :: :ok

put_device_tokens(state, hash, list)

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

put_pushed_request(state, t)

@callback put_pushed_request(state(), Sake.Auth.PushedRequest.t()) :: :ok

put_rotated_tokens(state, refresh_hash, list)

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

put_token(state, t)

@callback put_token(state(), Sake.Auth.TokenRecord.t()) :: :ok

revoke_family(state, t)

@callback revoke_family(state(), String.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.

revoke_token(state, t)

@callback revoke_token(state(), String.t()) :: :ok

tokens_for_subject(state, t)

@callback tokens_for_subject(state(), String.t()) :: [Sake.Auth.TokenRecord.t()]