Use Ithibati.Identity.Passkeys to add, list, rename and revoke credentials for an existing
account. Creating an account with its first credential is covered in
Getting started.
alias Ithibati.Identity.Passkeys
Passkeys.list_keys(account)
Passkeys.rename_key(account, id, "My work laptop")
Passkeys.delete_key(account, id)list_keys/1 returns the account's keys in stable, oldest-first order. Rename and delete return
{:ok, key} or {:error, reason}. A key belonging to another account is reported as
:not_found, just like a missing key.
Adding a passkey through Phoenix
Use the registration ceremony with a signed-in account as its subject. Add the gate after
fetch_session in the ceremony pipeline so the handler can read conn.assigns.current_account:
pipeline :ceremony do
plug :accepts, ["json"]
plug :fetch_session
plug :protect_from_forgery
plug Ithibati.Web.Gate, :current_account
endThe following callback clauses implement enrolment for existing accounts. In your handler,
alias Ithibati.Identity.Passkeys as Passkeys and import Phoenix.Controller.json/2:
@impl true
def registration_subject(conn, _params) do
case conn.assigns.current_account do
nil -> {:error, :not_signed_in}
account -> {:ok, account}
end
end
@impl true
def register(conn, key_attrs, %MyApp.Accounts.User{} = account, params) do
attrs = Passkeys.key_attrs(key_attrs, params["label"])
case Passkeys.add_key(account, attrs) do
{:ok, _key} -> {:ok, json(conn, %{status: "enrolled"})}
{:error, reason} -> {:error, reason}
end
endKeep authenticate/2 and recovered/3 in the handler as well. This registration_subject/2
requires a signed-in account; an application supporting both new registration and enrolment
must branch between its two policies or use separate mounts and handlers.
Read the account from the authenticated connection. Looking up an account from a posted username would let a caller enrol a passkey on another person's account.
Returning the account lets Ithibati populate excludeCredentials with its existing credentials.
The browser can then avoid registering one of those again.
From your page, push ithibati:register, optionally with %{label: label}. The hook returns
ithibati:done for the JSON response above; use that event to refresh the displayed keys.
The page needs the registration challenge and registration URL attributes described in
The browser hook.
Adding a passkey directly
Run registration with the existing account, then store only the verified result:
with {:ok, verified} <- Passkeys.verify_registration(credential, challenge) do
Passkeys.add_key(account, Passkeys.key_attrs(verified, "My phone"))
endThe surrounding challenge and transport flow is shown in
Without the web half. add_key/2 accepts the map from
verification directly; key_attrs/2 adds an optional label. Never pass unverified client data
as the key attributes.
A passkey added without a label receives the default label "Passkey". add_key/2 returns
{:error, :already_enrolled} if the credential is already stored, including on another account.
If the account has been deleted before the insert, the function raises.
Revoking a passkey
delete_key/2 refuses the account's last passkey with {:error, :last_key}. This also holds
when two callers try to delete different keys concurrently: deletion locks the account row
before checking how many remain.
To allow removal of the final passkey explicitly:
Passkeys.delete_key(account, id, last: :allow)last: accepts :refuse (the default) or :allow; another value raises ArgumentError.
An account without passkeys depends on its recovery codes for access. Make that
consequence clear in the application's removal flow.