# `Ithibati.Identity.Passkeys`
[🔗](https://github.com/oliverandrich/ithibati/blob/v0.6.2/lib/ithibati/identity/passkeys.ex#L1)

Registers and verifies passkeys and manages an account's stored credentials.

Registration uses `registration_challenge/3`, `registration_options/3` and
`verify_registration/2`. Authentication uses `authentication_challenge/3`,
`authentication_options/1` and `verify_authentication/2`.

Registration verification returns attributes for a new credential; authentication verification
returns the account that owns it. The caller decides what to store or issue afterwards.
Neither verification function creates a session.

Use `add_key/2`, `list_keys/1`, `rename_key/3` and `delete_key/3` for an existing account's
passkeys. Create an account with its first passkey and recovery codes through
`Ithibati.Identity.Grant.with_key_and_codes/3`.

[Registering and signing in](ceremonies.md) shows both the Phoenix integration and direct calls.

# `add_key`

Stores a verified passkey on an existing account.

`account` must be a struct of the configured account schema. `key_attrs` must contain the
verified `:key_id` and `:public_key`, with an optional `:label`. Use `verify_registration/2`
and optionally `key_attrs/2` to obtain these attributes.

Returns `{:ok, key}` or `{:error, :already_enrolled}` when the credential ID already exists,
including on another account. Other invalid changesets raise `Ecto.InvalidChangesetError`,
including a foreign-key failure if the account was deleted before the insert.

Labels are truncated to `Ithibati.UserKey.label_max/0` graphemes and trimmed. A missing or blank
label is stored as `"Passkey"`.

For a new account, compose `Ithibati.Identity.Grant.with_key_and_codes/3` into its creation
transaction instead.

# `authentication_challenge`

Returns `{:ok, challenge}` for authentication, or `{:error, :no_credentials}` if no passkey exists.

The existence check covers the entire configured credential table. Credentials are not
partitioned by relying-party ID or tenant in that table. This check is independent of
`Ithibati.Identity.Instance.needs_setup?/0`, which reports the bootstrap claim state.

`rp_id`, `origin` and the options have the same meanings as in `registration_challenge/3`.
Ithibati explicitly disables silent authentication, so a `wax_` application setting cannot
allow assertions without user presence.

Retain the challenge for `verify_authentication/2` and enforce single use on every attempt.

# `authentication_options`

Returns `PublicKeyCredentialRequestOptions` with binary fields encoded as unpadded base64url.

`allowCredentials` is empty so the browser can offer discoverable passkeys for the relying
party. User verification and timeout come from the challenge; timeout is converted to
milliseconds for the browser.

# `delete_key`

Deletes a passkey belonging to the account and returns `{:ok, key}`.

Returns `{:error, :not_found}` for a missing key, a key owned by another account or an invalid
UUID. By default, deleting the account's last passkey returns `{:error, :last_key}`. This
protection also applies to concurrent deletions through this function.

The `:last` option accepts `:refuse` (default) or `:allow`. Other values raise `ArgumentError`.
With `last: :allow`, an account can be left without passkeys and must use recovery codes to
sign in. If no credentials remain anywhere in the deployment, `authentication_challenge/3`
returns `{:error, :no_credentials}`.

# `key_attrs`

Adds `label` to verified credential attributes.

Accepts the map returned by `verify_registration/2` and returns a map containing `:key_id`,
`:public_key` and `:label`. Label trimming, truncation and the `"Passkey"` fallback are applied
when the credential is stored, not by this function.

# `list_keys`

Returns the account's passkeys as a list of `Ithibati.UserKey` structs.

Rows are ordered by `inserted_at`, then `id`, both ascending. The ID provides a stable order
when timestamps are equal. Each row includes its label and `last_used_at` value.

# `registration_challenge`

Returns a `Wax.Challenge` for passkey registration.

`rp_id` is the relying-party ID. `origin` is an expected origin or a non-empty list of accepted
origins. Supply both explicitly from trusted application settings; do not reflect client input.

## Options

  * `:user_verification` — `"required"`, `"preferred"` (default) or `"discouraged"`.
    These values are strings, not atoms.
  * `:seconds` — challenge lifetime as a positive integer; defaults to `60`.

Invalid option values raise `ArgumentError`. Registration does not require an existing account
or credential, so this function returns the challenge directly rather than an `:ok` tuple.

Ithibati requests no attestation and does not verify authenticator trust roots. It supplies the
WebAuthn options it depends on explicitly so `wax_` application defaults do not alter them.

Retain the challenge for `verify_registration/2`. The caller must enforce single use, including
when verification fails.

# `registration_options`

Returns `PublicKeyCredentialCreationOptions` with binary fields encoded as unpadded base64url.

Pass the configured account struct when adding a passkey, or an approved identifier when
creating an account. An account populates `excludeCredentials` from its stored passkeys;
an identifier produces an empty exclusion list.

The required `:rp_name` option supplies the display name of the relying party. User verification,
attestation and timeout are taken from the challenge. The browser timeout is in milliseconds.

Ithibati requires discoverable credentials because sign-in does not ask for an identifier.
It sets both `residentKey` and `requireResidentKey` and requests the `credProps` extension.
The offered algorithms are ES256 and RS256.

# `rename_key`

Renames a passkey belonging to the account and returns `{:ok, key}`.

The label is truncated to `Ithibati.UserKey.label_max/0` graphemes, then trimmed. Blank and
non-string values become `"Passkey"`. The update also changes `updated_at`.

Returns `{:error, :not_found}` for a missing key, a key owned by another account or an invalid
UUID. The update is scoped to the supplied account and cannot transfer ownership.

# `verify_authentication`

Verifies an assertion and returns `{:ok, account}` or `{:error, reason}`.

`credential` is the decoded JSON object produced by the browser's `credential.toJSON()`.
Pass the retained authentication challenge as the second argument. The returned account is
an instance of the configured account schema; no session or token is issued.

On success, the credential's `last_used_at` is updated without changing `updated_at`.
A credential removed before that update is reported as `:unknown_credential`.

Library validation errors use atom reasons. `wax_` errors may be exception structs; see
`verify_registration/2` for error handling. Ithibati does not store or compare authenticator
signature counters.

> #### Consume the challenge once {: .warning}
>
> The caller owns challenge storage and must prevent reuse, including concurrent attempts.
> A failed verification must also consume the challenge. Leaving it available permits another
> attempt with the same assertion while it remains valid.

# `verify_registration`

Verifies a registration credential and returns attributes ready to store.

`credential` is the decoded JSON object produced by the browser's `credential.toJSON()`;
its keys are strings and its binary fields are base64url-encoded. Pass the retained
registration challenge as the second argument.

Returns `{:ok, %{key_id: binary, public_key: binary}}` or `{:error, reason}`. Pass successful
attributes to `add_key/2` or `Ithibati.Identity.Grant.with_key_and_codes/3`; use `key_attrs/2`
to add a label first. Verification does not write a credential.

Library validation failures use atom reasons, including `:malformed_credential`,
`:not_discoverable`, `:no_attested_credential` and `:credential_id_too_long`. Errors from
`wax_` can be exception structs. Handle known atoms and provide a fallback for other reasons.

An explicit `false` or `"false"` in `clientExtensionResults.credProps.rk` is rejected. An absent
extension result is accepted for clients that do not report discoverability.

The caller must consume the stored challenge on every verification attempt, successful or not.

---

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