# `AshAuthentication.Strategy.WebAuthn`
[🔗](https://github.com/team-alembic/ash_authentication/blob/main/lib/ash_authentication/strategies/webauthn.ex#L5)

Strategy for authenticating using [WebAuthn/FIDO2](https://webauthn.io/) hardware
security keys and passkeys.

This strategy supports:

- Hardware security keys (YubiKey, etc.)
- Platform authenticators (Touch ID, Windows Hello, Face ID)
- Discoverable credentials / passkeys
- Multi-tenancy (dynamic `rp_id` per tenant)

Credentials are stored in a separate Ash resource that you define. The strategy
auto-generates actions on both the user resource and the credential resource for
registration, sign-in, credential management, and challenge generation.

## Modes

The strategy can be configured for two roles via the `registration_enabled?`,
`sign_in_enabled?`, and `verify_enabled?` flags:

- **Primary** (default; all three flags `true`) — passkeys are the primary
  credential. Users register and sign in directly with their authenticator.
- **Second factor** (`registration_enabled? false`, `sign_in_enabled? false`,
  `verify_enabled? true`) — passkeys are only used as a second factor on top
  of another primary credential (e.g. password). The strategy doesn't
  register or sign in users directly; it only verifies an assertion against
  the *currently authenticated* user. On successful verification, a
  `webauthn_verified_at` claim is added to the user's authentication token
  so protected routes can require it.

See the
[Passkeys as 2FA](https://hexdocs.pm/ash_authentication_phoenix/webauthn-2fa.html)
guide for the second-factor flow end to end.

## Quick Start

```elixir
defmodule MyApp.Accounts.User do
  use Ash.Resource,
    extensions: [AshAuthentication],
    domain: MyApp.Accounts

  attributes do
    uuid_primary_key :id
    attribute :email, :ci_string, allow_nil?: false
  end

  authentication do
    tokens do
      enabled? true
      token_resource MyApp.Accounts.Token
      signing_secret fn _, _ -> {:ok, Application.get_env(:my_app, :token_secret)} end
    end

    strategies do
      webauthn :webauthn do
        credential_resource MyApp.Accounts.Credential
        rp_id "example.com"
        rp_name "My App"
        origin "https://example.com"
        identity_field :email
      end
    end
  end

  relationships do
    has_many :webauthn_credentials, MyApp.Accounts.Credential
  end

  identities do
    identity :unique_email, [:email]
  end
end
```

## Origin Configuration

The **origin** is the full URL the browser sends during WebAuthn ceremonies
(scheme + domain + port). The **rp_id** is the domain name only. These are
related but distinct:

| Setting   | Example                    | What it is                       |
|-----------|----------------------------|----------------------------------|
| `rp_id`   | `"example.com"`            | Domain only (Relying Party ID)   |
| `origin`  | `"https://example.com"`    | Full URL including scheme + port |

If `origin` is not set, it defaults to `"https://{rp_id}"`. This works for
production on standard port 443, but **breaks in development** because the
browser includes the port in the origin and `Wax` will reject the mismatch.

### Production

    origin "https://example.com"

### Development (non-standard port)

    origin "https://localhost:4001"

### Multi-tenant (dynamic per tenant)

    origin {MyApp.WebAuthn, :origin_for_tenant, []}

## Credential Resource

You must define a separate Ash resource to store WebAuthn credentials. It needs:

- `credential_id` (`:binary`) - the raw credential ID from the authenticator
- `public_key` (`AshAuthentication.Strategy.WebAuthn.CoseKey`) - the COSE public key
- `sign_count` (`:integer`) - replay attack counter
- `label` (`:string`) - user-facing name for the credential
- `last_used_at` (`:utc_datetime_usec`, optional) - tracks last authentication time
- A `belongs_to` relationship to your user resource
- A policy bypass for `AshAuthentication.Checks.AshAuthenticationInteraction`

### Full Example

```elixir
defmodule MyApp.Accounts.Credential do
  use Ash.Resource,
    domain: MyApp.Accounts,
    data_layer: AshPostgres.DataLayer,
    authorizers: [Ash.Policy.Authorizer]

  postgres do
    table "webauthn_credentials"
    repo(MyApp.Repo)
  end

  policies do
    bypass AshAuthentication.Checks.AshAuthenticationInteraction do
      authorize_if always()
    end

    policy always() do
      authorize_if always()
    end
  end

  attributes do
    uuid_primary_key :id
    attribute :credential_id, :binary, allow_nil?: false, public?: true

    attribute :public_key, AshAuthentication.Strategy.WebAuthn.CoseKey,
      allow_nil?: false, public?: true

    attribute :sign_count, :integer, default: 0, allow_nil?: false, public?: true
    attribute :label, :string, default: "Security Key", public?: true
    attribute :last_used_at, :utc_datetime_usec, public?: true
    create_timestamp :inserted_at
    update_timestamp :updated_at
  end

  relationships do
    belongs_to :user, MyApp.Accounts.User, allow_nil?: false, public?: true
  end

  identities do
    identity :unique_credential_id, [:credential_id]
  end

  actions do
    defaults [:read, :destroy]

    create :create do
      primary? true
      accept [:credential_id, :public_key, :sign_count, :label, :user_id]
    end

    update :update do
      primary? true
      accept [:sign_count, :label, :last_used_at]
    end
  end
end
```

## Token Configuration

Tokens **must** be enabled for WebAuthn to work. The `signing_secret` callback
must return an `{:ok, value}` tuple, not a raw string:

```elixir
authentication do
  tokens do
    enabled? true
    token_resource MyApp.Accounts.Token
    signing_secret fn _, _ -> {:ok, Application.get_env(:my_app, :token_secret)} end
  end
end
```

## Adding Credentials to Existing Users

The built-in `register` action creates a **new user** with a credential. To add
a passkey to an already-authenticated user, you need a custom controller that:

1. Generates a registration challenge (via `Wax.new_registration_challenge/1`)
2. Sends it to the browser
3. Receives the attestation response
4. Calls `Wax.register/3` to verify it
5. Stores the credential directly on the credential resource

This is intentional -- the strategy's register flow is for new user sign-up,
not for adding keys to existing accounts.

## Accessing the User After Authentication

After successful WebAuthn sign-in, the JWT is available in user metadata:

```elixir
token = user.__metadata__[:token]
```

To load a user from a token (e.g., in a LiveView `mount`):

```elixir
{:ok, user} = AshAuthentication.subject_to_user(
  "user?id=#{user_id}",
  MyApp.Accounts.User
)
```

## Multi-Tenancy

For multi-tenant applications, `rp_id`, `rp_name`, and `origin` all accept
MFA tuples that receive the tenant as the first argument:

```elixir
webauthn :webauthn do
  credential_resource MyApp.Accounts.Credential
  rp_id {MyApp.WebAuthn, :rp_id_for_tenant, []}
  rp_name {MyApp.WebAuthn, :rp_name_for_tenant, []}
  origin {MyApp.WebAuthn, :origin_for_tenant, []}
  identity_field :email
end
```

Then implement the callbacks:

```elixir
defmodule MyApp.WebAuthn do
  def rp_id_for_tenant(tenant), do: "#{tenant}.example.com"
  def rp_name_for_tenant(tenant), do: "MyApp - #{tenant}"
  def origin_for_tenant(tenant), do: "https://#{tenant}.example.com"
end
```

## Gotchas

- **Origin must include the port** for non-standard ports (e.g., `"https://localhost:4001"`).
  The default derivation from `rp_id` produces `"https://{rp_id}"` which omits the port.
- **Signing secret must return `{:ok, value}`**, not a raw string. A common mistake
  is `fn _, _ -> "my_secret" end` -- it must be `fn _, _ -> {:ok, "my_secret"} end`.
- **Challenge data is stored in the session as plain maps**, not `Wax.Challenge` structs,
  because cookie session stores cannot serialize arbitrary Elixir structs. The plug
  reconstructs the struct before passing it to Wax.
- **`add_credential` (adding a key to an existing user) is not built-in.** The `register`
  action creates a new user. See "Adding Credentials to Existing Users" above.
- **`origin_verify_fun`** is hardcoded to `{Wax, :origins_match?, []}` when
  reconstructing challenges from the session. If you need custom origin verification,
  you'll need to override the plug.
- **Token generation happens in `Actions.sign_in`** via `Jwt.token_for_user/3`, not in
  an Ash preparation like the Password strategy. This is because Wax verification
  happens outside the Ash action pipeline.

# `t`

```elixir
@type t() :: %AshAuthentication.Strategy.WebAuthn{
  __spark_metadata__: any(),
  add_credential_action_name: atom() | nil,
  attestation: String.t(),
  authenticator_attachment: nil | :platform | :cross_platform,
  credential_id_field: atom(),
  credential_resource: module(),
  credentials_relationship_name: atom(),
  delete_credential_action_name: atom() | nil,
  identity_field: atom(),
  label_field: atom(),
  last_used_at_field: atom() | nil,
  list_credentials_action_name: atom() | nil,
  name: atom(),
  origin: String.t() | {module(), atom(), list()} | {module(), keyword()} | nil,
  provider: :webauthn,
  public_key_field: atom(),
  register_action_name: atom() | nil,
  registration_enabled?: boolean(),
  resident_key: :required | :preferred | :discouraged,
  resource: module(),
  rp_id: String.t() | {module(), atom(), list()} | {module(), keyword()},
  rp_name: String.t() | {module(), atom(), list()} | {module(), keyword()},
  sign_count_field: atom(),
  sign_in_action_name: atom() | nil,
  sign_in_enabled?: boolean(),
  sign_in_with_token_action_name: atom() | nil,
  store_credential_action_name: atom() | nil,
  timeout: pos_integer(),
  update_credential_label_action_name: atom() | nil,
  update_sign_count_action_name: atom() | nil,
  user_relationship_name: atom(),
  user_verification: String.t(),
  verify_action_name: atom() | nil,
  verify_enabled?: boolean()
}
```

# `transform`

# `verify`

---

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