# `PhoenixKit.API.Keys`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.64.1/lib/phoenix_kit/api/keys.ex#L1)

API keys: creating, verifying, revoking and rotating the bearer tokens of the
admin API.

A key belongs to a **user** (its owner) and carries a list of **scopes**
(`"catalogue.products:write"`). What a request may actually do is the key's
scopes filtered by the owner's permissions **right now**:

    effective scopes = key.scopes
                       that some enabled admin tool declares
                       and whose tool permission the owner holds

The owner's permissions come from `Scope.for_user(user, narrow: false)` (the
union of all roles, ignoring the role switcher) and `Scope.can?/2`, on every
request. Removing a role from the owner, blocking or deleting the owner
therefore takes effect on the next request, whatever the 60 second key row
cache still holds (`PhoenixKit.API.Keys.Cache`). A scope whose module is
disabled stays on the key but grants nothing.

## Who may create a key

- the creator holds the `api` permission;
- the owner is the creator, or a **service user** (`api_only`) they created
  (an Owner may use any) whose roles' permissions are a subset of the creator's;
- every scope is one the owner could use (`allowed_scopes_for/1`).

Authorization failures are atoms (`:forbidden`, `:owner_not_allowed`,
`:owner_inactive`, `:invalid_scopes`); validation failures are changesets.

## Token

`pk_<8 hex>_<43 base64url>` (see `PhoenixKit.API.Keys.Token`). The token is
returned once, by `create_key/3` and `rotate/3`; only a SHA-256 of its secret
is stored.

## Revocation and rotation

`revoked_at` in the past means revoked. A `revoked_at` **in the future** is a
scheduled revocation: the key works until then. `rotate/3` uses it to keep the
old key alive for a grace period.

`rotate/3` takes the person's scope and checks it on the locked row: the person
must be able to manage the key (`can_manage?/2`) and, when the owner is a
service user, must still hold every permission that service user holds (the
rule of `create_key/3`), so a demoted creator cannot mint a token beyond their
own rights. The new token acts as the key's owner. When an Owner rotates the
key of a regular user, the token they receive therefore acts as that user;
this is intended.

`revoke/2` is different: it takes a bare actor uuid and does not authorize.
The caller decides with `can_manage?/2` first (the Settings page does, by
finding the key through `list_keys/1`).

## Editing scopes

`update_scopes/3` replaces the scope list of a live key. The editor must be
able to manage the key, and every new scope must be one the key's **owner**
could be given (`allowed_scopes_for/1`). When the editor is not the owner, a new
scope must also be one the editor holds themselves (the permissions of its
tools): a creator who was demoted, or a service user whose roles grew, cannot
widen the key beyond the editor's own rights. Scopes already on the key may stay
or go whoever the editor is. The cached row is dropped, so the next request
already sees the new list.

Scopes the owner cannot use right now (module switched off, tool hidden,
permission lost, owner blocked or deleted) are not the editor's to change: they
are merged back into the saved list and stay on the key, granting nothing until
the owner can use them again. Authorization and validation run on the locked,
re-read row, never on the struct the caller holds.

Create, revoke, rotate and scope edits are written to `PhoenixKit.Activity`
(module `api`, `permanent: true`).

# `active?`

```elixir
@spec active?(PhoenixKit.API.Keys.Key.t()) :: boolean()
```

Token-less check that a key may be used now: the
key is neither revoked nor expired (`dead_reason/1` is `nil`) **and its owner is an
active user** (`Auth.get_user/1 |> Auth.ensure_active_user/1`, the same lookup
`verify/2` uses). A caller that needs the reason, or the key's effective scopes, uses
`Confirm.live_actor/1`. The owner is read only when the row itself is alive.

# `active_owner`

```elixir
@spec active_owner(PhoenixKit.API.Keys.Key.t()) ::
  {:ok, PhoenixKit.Users.Auth.User.t()} | {:error, :owner_inactive}
```

The active owner of a key, `{:error, :owner_inactive}` when the user is gone, blocked or
not confirmed. The one lookup behind `verify/2`, `active?/1` and `Confirm.live_actor/1`.

# `actor`

```elixir
@spec actor(
  PhoenixKit.API.Keys.Key.t(),
  PhoenixKit.Users.Auth.User.t(),
  String.t() | nil
) ::
  PhoenixKit.API.Actor.t()
```

The `Actor` of `key` held by its (already loaded, active) `owner`, with the effective
scopes. The one builder: `verify/2` and `Confirm.live_actor/1` both use it, so a field
added to the actor reaches the request path and an approved operation alike.

# `allowed_scopes_for`

```elixir
@spec allowed_scopes_for(PhoenixKit.Users.Auth.User.t()) :: [String.t()]
```

Every scope the owner could be given: a scope of the enabled admin tools, provided the
owner holds the permission of every one of those tools that declares it.

# `allowed_scopes_for`

```elixir
@spec allowed_scopes_for(
  PhoenixKit.Users.Auth.User.t(),
  PhoenixKit.Users.Auth.Scope.t()
) :: [
  String.t()
]
```

The scopes `editor_scope` may hand to `owner`'s keys: `allowed_scopes_for/1` of
the owner, narrowed to the scopes whose tool permissions the editor holds too.
Editing one's own keys needs no narrowing (the owner is the editor).

# `can_manage?`

```elixir
@spec can_manage?(PhoenixKit.Users.Auth.Scope.t(), PhoenixKit.API.Keys.Key.t()) ::
  boolean()
```

Whether the person behind `scope` may revoke or rotate `key`: they hold `api`
and are an Owner or created the key.

# `create_key`

```elixir
@spec create_key(
  PhoenixKit.Users.Auth.User.t(),
  PhoenixKit.Users.Auth.Scope.t(),
  map()
) ::
  {:ok, PhoenixKit.API.Keys.Key.t(), String.t()}
  | {:error, Ecto.Changeset.t() | atom()}
```

Creates a key for `owner` on behalf of the person behind `creator_scope`.

`attrs` takes `name` (required), `scopes`, `requires_approval`, `allowed_ips`
(CIDR blocks or bare addresses; stored in canonical form) and `expires_at`,
with atom or string keys (not mixed).

Returns the stored key and the token, which exists nowhere else and is never
shown again.

# `dead_reason`

```elixir
@spec dead_reason(PhoenixKit.API.Keys.Key.t()) :: :revoked | :expired | nil
```

Why a key row cannot be used now: `:revoked` (`revoked_at` has been reached), `:expired`
(`expires_at` has been reached), or `nil`. A time in the future has not been reached: a
revocation in the future is a scheduled one (the grace period of a rotation) and the key
still works until then. When both have been reached the answer is `:revoked`.

# `effective_scopes`

```elixir
@spec effective_scopes(PhoenixKit.API.Keys.Key.t() | PhoenixKit.API.Actor.t()) :: [
  String.t()
]
```

The scopes of a key (or of an `Actor`) that its owner may use **right now**.

Empty when the owner is blocked or deleted. For code that holds a `Key` row or
an old `Actor` (the Settings page, tests). A request does not need it: the
`Actor` returned by `verify/2` already carries these scopes and the Executor
trusts them.

# `get_key`

```elixir
@spec get_key(String.t()) :: PhoenixKit.API.Keys.Key.t() | nil
```

Fetches a key by uuid; `nil` for an unknown or malformed uuid.

# `held_scopes_of`

```elixir
@spec held_scopes_of(PhoenixKit.Users.Auth.Scope.t()) :: [String.t()]
```

The scopes `scope` holds: those of the enabled admin tools, provided `scope` holds the
permission of every one of those tools that declares it (the rule of
`allowed_scopes_for/1`), sorted.

`allowed_scopes_for/1` asks it of the owner's full scope (`narrow: false`); a caller
acting through a live session (`PhoenixKit.API.Executor.execute_as/5`) asks it of the
session's scope, which is narrowed to the session's active role.

# `list_keys`

```elixir
@spec list_keys(PhoenixKit.Users.Auth.Scope.t()) :: [PhoenixKit.API.Keys.Key.t()]
```

The keys the person behind `scope` may see: all of them for an Owner, the keys
they created themselves for any other holder of `api`, none otherwise.
Newest first.

# `revoke`

```elixir
@spec revoke(PhoenixKit.API.Keys.Key.t(), String.t() | nil) ::
  {:ok, PhoenixKit.API.Keys.Key.t()} | {:error, term()}
```

Revokes a key now. Revoking a key that is already revoked changes nothing.
The row is read again, so a stale struct neither overwrites an earlier
revocation nor fails; a key that no longer exists is `{:error, :not_found}`.

Unlike `rotate/3` and `update_scopes/3`, which take the person's scope and
authorize it, this takes a bare `actor_uuid` (only for the activity entry) and
checks nothing: the CALLER must authorize with `can_manage?/2` first (the
Settings page does, by finding the key through `list_keys/1`).

# `rotate`

```elixir
@spec rotate(PhoenixKit.API.Keys.Key.t(), 0..24, PhoenixKit.Users.Auth.Scope.t()) ::
  {:ok, PhoenixKit.API.Keys.Key.t(), String.t()} | {:error, term()}
```

Issues a new key with the same owner, scopes, flags, IP list and expiry, and
retires the old one after `grace_hours` (0 to 24; 0 revokes it now).

The old key keeps working until then, so a client can switch over without
downtime. The creator must be able to manage the key (`can_manage?/2`) and, when the owner
is a service user, still hold every permission the service user holds
(`{:error, :owner_not_allowed}`, as in `create_key/3`). Returns the new key and
its token.

The row is locked and read again first. A key that is gone is
`{:error, :not_found}`, one that is revoked or expired `:revoked` / `:expired`
(also through a stale struct), and one whose retirement is already scheduled
`:rotation_pending`.

# `token_prefix`

```elixir
@spec token_prefix(String.t()) :: String.t() | nil
```

The prefix of a token (`"pk_<prefix>_<secret>"` gives `prefix`), or `nil`.
Safe to log: the prefix is not secret.

# `update_scopes`

```elixir
@spec update_scopes(
  PhoenixKit.API.Keys.Key.t(),
  [String.t()],
  PhoenixKit.Users.Auth.Scope.t()
) ::
  {:ok, PhoenixKit.API.Keys.Key.t()}
  | {:error, :forbidden | :invalid_scopes | :revoked | :not_found}
```

Replaces the scopes of a live key.

- `editor_scope` must be able to manage the key (`can_manage?/2`), else
  `{:error, :forbidden}`;
- the key is re-read: gone gives `{:error, :not_found}`, revoked (even if the
  struct passed in is stale) `{:error, :revoked}`;
- `new_scopes` is the desired list of **active** scopes: a list of strings, and
  every scope that is not on the key yet must be in `allowed_scopes_for/1` of the
  key's **owner** and, unless the editor is the owner, in `allowed_scopes_for/2`
  (the editor holds the permissions of its tools too), else
  `{:error, :invalid_scopes}`. Blank entries and duplicates
  are dropped; an empty list is allowed;
- scopes already on the key that the owner cannot use right now are **kept
  untouched**: their module is switched off, their tool is hidden, the owner lost
  the permission, or the owner is blocked or deleted (then every scope is inactive
  and saving `[]` changes nothing). Saving never drops them, whether or not
  `new_scopes` names them. A named inactive scope keeps the position requested for
  it; the others are appended after the requested scopes. An inactive scope cannot
  be removed while it is inactive: only `revoke/2` removes it (`rotate/3` copies
  it). It grants nothing meanwhile (`effective_scopes/1`) and works again once the
  owner can use it.

On success the cached row is dropped on every node and a permanent
`api.key_scopes_updated` entry (`from` / `to`) is written. Saving the scopes the
key already has, in any order, changes and logs nothing.

# `verify`

```elixir
@spec verify(term(), String.t() | nil) ::
  {:ok, PhoenixKit.API.Actor.t()}
  | {:error,
     :invalid_token | :revoked | :expired | :ip_not_allowed | :owner_inactive}
```

Verifies a bearer token presented from `ip`.

Checks, in order: the token's shape and secret, revocation, expiry, the IP
allow-list, and that the owner exists and is active. On success returns an
`Actor` whose `scopes` are the key's **effective** scopes for this request.

Never raises on bad input, whatever its type (a missing header arrives as
`nil`); a malformed token is `:invalid_token`, the same answer as an unknown
key, so the response does not tell the two apart.

The returned `Actor` already carries the **effective** scopes of this request
(the key's scopes filtered by the owner's live permissions and by the tools
that are enabled right now), and `verify/2` itself writes `last_used_at` /
`last_used_ip`. The Executor therefore trusts `actor.scopes` and does
not call `effective_scopes/1` again for the same request: the owner lookup, the
`Scope.for_user/2` build and the tool-registry scan happen once, here.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
