# `MishkaCmsCore.Captcha.Provider`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/captcha/provider.ex#L5)

A captcha provider: one module that is a `mishka_installer` plugin on the `"captcha:providers"`
event and implements the callbacks below. `MishkaCmsCore.Captcha` never names a provider: a scope
picks one on the settings page's Security tab (`security.captcha.provider`), and its sign-in,
registration, forgot-password and sign-in-link forms ask it.

    defmodule MyExtension.Captcha.Acme do
      use MishkaCmsCore.Captcha.Provider, id: "acme", label: "Acme Captcha"

      @site_key "security.captcha.acme.site_key"
      @secret "security.captcha.acme.secret_key"

      @impl true
      def settings do
        [
          site_key(@site_key, gettext_noop("Acme site key"), group: "Acme Captcha"),
          secret(@secret, gettext_noop("Acme secret key"), group: "Acme Captcha")
        ]
      end

      @impl true
      def ready?(site_id), do: own(@site_key, site_id) != nil and secret?(@secret, site_id)

      @impl true
      def browser(site_id, _form, _language) do
        %{
          script: "https://captcha.acme.example/api.js?render=explicit",
          global: "acme",
          mode: :widget,
          options: %{"sitekey" => own(@site_key, site_id)}
        }
      end

      @impl true
      def csp, do: %{script_src: ["https://captcha.acme.example"], frame_src: ["https://captcha.acme.example"]}

      @impl true
      def verify(token, site_id, request) do
        with {:ok, answer} <-
               siteverify("https://captcha.acme.example/siteverify", %{
                 secret: Settings.secret(@secret, site_id),
                 response: token,
                 remoteip: request.remote_ip
               }),
             do: {:ok, %{hostname: answer["hostname"], test?: false}}
      end
    end

Supervise the module in the extension's application, like any other plugin. `use` adds the plugin
half — the `"captcha:providers"` registration, see `MishkaCmsCore.ProviderRegistry` — plus
`site_key/3` and `secret/3`, which build a provider's settings the way the core's are built,
`own/2`, which reads one, and `siteverify/2`, which asks a provider's server about a token.

## Settings

`settings/0` declares the provider's own fields, shown on the Security tab under its label. Each
scope sets its own (`resolve: :own`): a site never checks with another site's keys. The secret is a
`:secret` setting; read it with `MishkaCmsCore.Settings.secret/2`, never `get/3`.

## The browser

`browser/3` tells the one `Captcha` hook (`assets/js/shared_hooks.js`) how to draw the check for
`form` in `language`: the `script` to load, the `global` it defines, and the `mode` —

  * `:widget` — a box drawn with `global.render(element, options)`, whose `callback` hands back
    the token (Turnstile, reCAPTCHA v2, hCaptcha);
  * `:score` — nothing drawn: `global.execute(options.sitekey, %{action: options.action})` is asked
    for a token as the form is sent (reCAPTCHA v3).

`csp/0` names the origins the browser loads the provider from; the security headers allow them on
the sites that use it, and nowhere else.

## Verifying

`verify/3` asks the provider's server about one token, for `site_id`, with `request` —
`%{remote_ip: address | nil, form: form}` — through `siteverify/2`, which reads the answer the same
way for every provider. It answers `{:ok, %{hostname: host, test?: boolean}}`: the address the
check was solved on, and whether the scope checks with the provider's published test keys, which
pass every token on any address; or `{:error, {:failed, detail}}` when the token was refused —
wrong, expired, used, a low score — or `{:error, {:unavailable, detail}}` when the provider could
not be asked or refused the keys themselves. `detail` is a short text for the log and the settings
page's test.

## The registered providers

`all/0`, `offered/0` and `fetch/1` ask the `"captcha:providers"` event which providers are
installed: the core's own — Cloudflare Turnstile, Google reCAPTCHA and hCaptcha — register the
same way an extension's would.

# `browser`

```elixir
@type browser() :: %{
  script: String.t(),
  global: String.t(),
  mode: :widget | :score,
  options: %{required(String.t()) =&gt; term()}
}
```

# `form`

```elixir
@type form() :: :sign_in | :register | :reset_request | :magic_link | :test | atom()
```

The form a check is for: the sign-in pages' four, the settings page's `:test`, or an extension's
own, named by the atom its action passes `MishkaCmsCore.Accounts.CaptchaCheck`.

# `site_id`

```elixir
@type site_id() :: String.t() | nil
```

# `browser`

```elixir
@callback browser(site_id(), form(), language :: String.t()) :: browser()
```

How the browser draws the check for `form` on `site_id`, in `language`.

# `csp`

```elixir
@callback csp() :: %{
  optional(:script_src | :frame_src | :style_src | :connect_src) =&gt; [String.t()]
}
```

The origins the browser loads the provider from, by CSP directive.

# `id`

```elixir
@callback id() :: String.t()
```

The provider's id, stored in `security.captcha.provider`.

# `label`

```elixir
@callback label() :: String.t()
```

The name on the settings page, as a `gettext_noop/1` msgid.

# `offered?`
*optional* 

```elixir
@callback offered?() :: boolean()
```

Whether the provider is offered on this installation. Every provider is, unless it says not.

# `ready?`

```elixir
@callback ready?(site_id()) :: boolean()
```

Whether `site_id` has everything this provider needs to check.

# `settings`

```elixir
@callback settings() :: [MishkaCmsCore.Settings.definition()]
```

The provider's own setting definitions (`MishkaCmsCore.Settings.define/3`).

# `verify`

```elixir
@callback verify(token :: String.t(), site_id(), %{
  remote_ip: String.t() | nil,
  form: form()
}) ::
  {:ok, %{hostname: String.t() | nil, test?: boolean()}}
  | {:error, {:failed | :unavailable, String.t()}}
```

Asks the provider about `token`.

# `all`

```elixir
@spec all() :: [module()]
```

Every registered provider module, the first of each id.

# `event`

```elixir
@spec event() :: String.t()
```

The event a provider plugin listens on.

# `fetch`

```elixir
@spec fetch(String.t() | nil) ::
  {:ok, module()} | {:error, :unknown_provider | :cannot_run}
```

The offered provider registered under `id`.

# `offered`

```elixir
@spec offered() :: [module()]
```

The providers this installation offers on the settings page.

# `own`

```elixir
@spec own(String.t(), site_id()) :: String.t() | nil
```

A scope's own value of a provider setting, trimmed; `nil` when it set none.

# `secret`

```elixir
@spec secret(String.t(), String.t(), keyword()) :: MishkaCmsCore.Settings.definition()
```

A provider's secret field: a `:secret` setting on the Security tab that each scope sets for
itself, stored sealed and never shown again. `opts` are `MishkaCmsCore.Settings.define/3`'s.

# `secret?`

```elixir
@spec secret?(String.t(), site_id()) :: boolean()
```

Whether a scope holds a `:secret` setting this server can open. One sealed under another
`SECRET_KEY_BASE` cannot be, and counts as none: the provider is not ready, nothing is asked, and
the settings page shows the key as not set until it is typed again.

# `site_key`

```elixir
@spec site_key(String.t(), String.t(), keyword()) ::
  MishkaCmsCore.Settings.definition()
```

A provider's public key field: a `:string` setting on the Security tab that each scope sets for
itself. `opts` are `MishkaCmsCore.Settings.define/3`'s.

# `siteverify`

```elixir
@spec siteverify(String.t(), map()) ::
  {:ok, map()} | {:error, {:failed | :unavailable, String.t()}}
```

Posts `fields` to a provider's verify endpoint, as a form, and reads the answer the way Turnstile,
reCAPTCHA and hCaptcha all give it: `{:ok, answer}` when it says `"success": true`;
`{:error, {:unavailable, detail}}` when it could not be reached in time, failed (a 5xx), or refused
the keys themselves (`missing-input-secret`, `invalid-input-secret`, `sitekey-secret-mismatch`,
`not-using-dummy-passcode`, `internal-error`); `{:error, {:failed, detail}}` for every other answer —
a refusal with any other code or none, a 4xx, a body that is no answer. A field that is `nil` is
left out. No retry and no redirect, so a slow provider cannot hold a sign-in open and the secret is
never sent anywhere but `url`.

`config :mishka_cms_core, MishkaCmsCore.Captcha.Provider, req_options: [...]` adds to the request — the
test suite routes it to `Req.Test`.

---

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