# Getting started

Build a Phoenix application where someone registers a username and passkey, saves recovery
codes, opens a protected page and signs out. This guide uses open registration. Add
[invitation-only registration](invitations.md) once this flow works.

You need Elixir 1.18 or newer, PostgreSQL, Phoenix 1.8 and LiveView 1.1. The examples below use
the layouts, components and asset configuration generated by Phoenix 1.8. For SQLite, generate
the project with `--database sqlite3`; for MySQL, use `--database mysql`. Apply the corresponding
[database settings](configuration.md#databases) before migrating.

```console
$ mix phx.new my_app
$ cd my_app
$ mix ecto.create
```

Keep the files generated by Phoenix unless a step explicitly replaces them. For a complete
application to compare against, see the
[open-registration example](https://github.com/oliverandrich/ithibati/tree/main/examples/open_registration).

## 1. Add the dependency

Add Ithibati to the existing dependency list in `mix.exs`:

```elixir
{:ithibati, "~> 0.1"}
```

Then fetch it:

```console
$ mix deps.get
```

## 2. Configure it

Add this to `config/config.exs`, above its final `import_config` line:

```elixir
config :ithibati,
  repo: MyApp.Repo,
  user_schema: MyApp.Accounts.User,
  users_key_type: :id
```

`:id` matches the account table this guide creates. If your application uses UUID primary keys,
see [Configuration and schemas](configuration.md#primary-keys-and-table-names).
`users_key_type` and `table_prefix` are compile-time settings and belong in `config/config.exs`.

## 3. The account schema

Create `lib/my_app/accounts/user.ex`:

```elixir
defmodule MyApp.Accounts.User do
  use Ecto.Schema

  alias Ithibati.Schema.Identifier
  alias Ithibati.Schema.User

  use User, identifier: :username, format: Identifier.username_format()

  import Ecto.Changeset

  schema "users" do
    ithibati_account()

    field :name, :string
    timestamps(type: :utc_datetime_usec)
  end

  def changeset(user, attrs) do
    user
    |> identifier_changeset(attrs)
    |> cast(attrs, [:name])
  end
end
```

`ithibati_account()` declares `username` and the passkey, recovery-code and session associations.
The changeset trims and lowercases the username, requires it, and checks its format. The built-in
username format accepts ASCII letters, digits and underscores, up to thirty characters.

The `name` field belongs to this example application. Other identifier fields, custom formats
and display names are covered in [Configuration and schemas](configuration.md#identifiers).

## 4. The migration

Generate two migrations, in this order:

```console
$ mix ecto.gen.migration create_users
$ mix ecto.gen.migration add_ithibati
```

In the generated `*_create_users.exs` file under `priv/repo/migrations/`, create your account table:

```elixir
defmodule MyApp.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :username, :string, null: false
      add :name, :string

      timestamps(type: :utc_datetime_usec)
    end
  end
end
```

In `*_add_ithibati.exs`, create Ithibati's tables and the unique index on `users.username`:

```elixir
defmodule MyApp.Repo.Migrations.AddIthibati do
  use Ecto.Migration

  def up, do: Ithibati.Migration.up(version: 4)
  def down, do: Ithibati.Migration.down(version: 4)
end
```

Keep `version: 4` pinned. The account table must exist before this migration runs.
Ithibati checks its identifier and primary-key columns before creating its own tables.

```console
$ mix ecto.migrate
```

You now have the account table, the identifier's unique index, and Ithibati's passkey,
recovery-code, session and bootstrap tables. See [Migration behaviour](configuration.md#migrations)
for ownership, deletion and upgrade details.

## 5. The handler

Create `lib/my_app_web/auth.ex`. This handler approves registration, creates the account and its
credentials in one transaction, and signs in verified accounts:

```elixir
defmodule MyAppWeb.Auth do
  @behaviour Ithibati.Web.Handler

  import Phoenix.Controller, only: [json: 2]
  import Plug.Conn, only: [put_session: 3]

  alias Ecto.Multi
  alias Ithibati.Identity.Grant
  alias Ithibati.Web.Gate
  alias MyApp.Accounts.User
  alias MyApp.Repo

  @impl true
  def registration_subject(_conn, %{"username" => username}) do
    %User{}
    |> User.changeset(%{"username" => username})
    |> Ecto.Changeset.apply_action(:insert)
    |> case do
      {:ok, account} -> {:ok, account.username}
      {:error, _changeset} -> {:error, :invalid_username}
    end
  end

  def registration_subject(_conn, _params), do: {:error, :username_required}

  @impl true
  def register(conn, key_attrs, username, _params) do
    Multi.new()
    |> Multi.insert(:account, User.changeset(%User{}, %{"username" => username}))
    |> Grant.with_key_and_codes(key_attrs)
    |> Repo.transaction()
    |> case do
      {:ok, %{account: account, recovery_codes: codes}} ->
        {:ok,
         conn
         |> Gate.log_in(account)
         |> put_session(:recovery_codes, codes)
         |> json(%{redirect: "/recovery-codes"})}

      {:error, :account, %Ecto.Changeset{} = changeset, _changes} ->
        if Ithibati.Schema.User.identifier_taken?(changeset),
          do: {:error, :username_taken},
          else: {:error, :invalid_username}

      {:error, _step, reason, _changes} ->
        {:error, reason}
    end
  end

  @impl true
  def authenticate(conn, account),
    do: {:ok, conn |> Gate.log_in(account) |> json(%{redirect: "/inside"})}

  @impl true
  def recovered(conn, account, nil), do: authenticate(conn, account)

  def recovered(conn, account, fresh) do
    {:ok,
     conn
     |> Gate.log_in(account)
     |> put_session(:recovery_codes, fresh)
     |> json(%{redirect: "/recovery-codes"})}
  end
end
```

`registration_subject/2` validates the identifier before the browser opens its passkey dialog.
`register/4` receives that approved identifier as its third argument. Use it when creating the
account; request parameters are sent again and may have changed.

`Grant.with_key_and_codes/3` expects an account step named `:account` and adds `:passkey` and
`:recovery_codes`. Application steps that can refuse should go before the grant. Avoid logging
whole transaction results: they can contain plaintext recovery codes.

`recovered/3` receives a fresh batch only when the last unused recovery code was spent. This
handler sends it to the same page used after registration. After `Gate.log_in/2`, finish with a
full page load so the browser receives the renewed session and CSRF token. The hook follows
the JSON `redirect` value for you.

## 6. Showing the recovery codes

Create `lib/my_app_web/controllers/session_controller.ex`:

```elixir
defmodule MyAppWeb.SessionController do
  use MyAppWeb, :controller

  alias Ithibati.Web.Gate

  def recovery_codes(conn, _params) do
    case get_session(conn, :recovery_codes) do
      nil -> redirect(conn, to: "/inside")
      codes -> conn |> delete_session(:recovery_codes) |> render(:recovery_codes, codes: codes)
    end
  end

  def sign_out(conn, _params), do: conn |> Gate.log_out() |> redirect(to: "/")
end
```

The controller removes the temporary session entry when it renders the codes. The database
stores only their digests, so the application cannot retrieve the batch later. This is an HTTP
controller because it must update the session cookie.

Create its HTML module at `lib/my_app_web/controllers/session_html.ex`:

```elixir
defmodule MyAppWeb.SessionHTML do
  use MyAppWeb, :html

  embed_templates "session_html/*"
end
```

Create `lib/my_app_web/controllers/session_html/recovery_codes.html.heex`:

```heex
<Layouts.app flash={%{}}>
  <.header>
    Your recovery codes
    <:subtitle>
      Twelve, each good for one sign-in, and this is the only time they are shown.
    </:subtitle>
  </.header>

  <div class="alert alert-warning mt-6">
    <span>Save these codes somewhere safe. Use one if you lose access to your passkeys.</span>
  </div>

  <ul class="mt-6 grid grid-cols-2 gap-2 font-mono text-sm">
    <li :for={code <- @codes} class="rounded bg-base-200 px-3 py-2">{code}</li>
  </ul>

  <p class="mt-6"><.link href={~p"/inside"} class="link">Done</.link></p>
</Layouts.app>
```

The default batch contains twelve codes, each good for one sign-in. [Recovery codes](recovery.md)
explains regeneration and automatic refill.

## 7. The routes

In `lib/my_app_web/router.ex`, import `Ithibati.Web.Router`, add the gate to the existing browser
pipeline, and add the ceremony pipeline and routes below.

Replace the generated `get "/", PageController, :home` route with the public LiveView route.
Keep any other generated scopes, including the `dev_routes` block; they are omitted here.

```elixir
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  import Ithibati.Web.Router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug Ithibati.Web.Gate, :current_account
  end

  pipeline :ceremony do
    plug :accepts, ["json"]
    plug :fetch_session
    plug :protect_from_forgery
  end

  scope "/auth" do
    pipe_through :ceremony
    ithibati_routes handler: MyAppWeb.Auth, rp_name: "MyApp"
  end

  scope "/", MyAppWeb do
    pipe_through :browser

    live_session :public, on_mount: [{Ithibati.Web.Gate, :current_account}] do
      live "/", SignInLive
    end

    live_session :members, on_mount: [{Ithibati.Web.Gate, {:require_account, to: "/"}}] do
      live "/inside", InsideLive
    end

    get "/recovery-codes", SessionController, :recovery_codes
    delete "/session", SessionController, :sign_out
  end
end
```

The ceremony pipeline accepts JSON, fetches the session that holds the challenge, and checks
CSRF protection. Reusing the HTML-only browser pipeline would reject the hook's requests.

`Gate` assigns `@current_account` in both HTTP requests and LiveView mounts. The members session
requires an account and redirects visitors to `/` when none is present.

## 8. The browser

In `assets/js/app.js`, import Ithibati's hooks and merge them into the existing `LiveSocket`
configuration. Preserve the other generated options and hooks:

```javascript
import {hooks as ithibatiHooks} from "ithibati"

let liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  hooks: {...colocatedHooks, ...ithibatiHooks},
})
```

The package import works with the Hex dependency and Phoenix 1.8's generated esbuild setup.
For a local path dependency or the colocated manifest, see
[Browser imports](ceremonies.md#browser-imports).

## 9. The page people register and sign in on

Create `lib/my_app_web/live/sign_in_live.ex` and its parent directory. The registration form
collects a username. Sign-in needs only a button: the browser lets the person choose a passkey.
The recovery form sends a code to the recovery endpoint.

```elixir
defmodule MyAppWeb.SignInLive do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket), do: {:ok, assign(socket, username: "", error: nil)}

  @impl true
  def handle_event("validate", %{"username" => username}, socket),
    do: {:noreply, assign(socket, username: username, error: nil)}

  def handle_event("register", %{"username" => username}, socket),
    do:
      {:noreply,
       socket |> assign(error: nil) |> push_event("ithibati:register", %{username: username})}

  def handle_event("sign-in", _params, socket),
    do: {:noreply, socket |> assign(error: nil) |> push_event("ithibati:authenticate", %{})}

  def handle_event("recover", %{"code" => code}, socket),
    do: {:noreply, socket |> assign(error: nil) |> push_event("ithibati:recover", %{code: code})}

  # A successful ceremony ends in the redirect the handler answered with, so only failures arrive
  # back here.
  def handle_event("ithibati:failed", %{"error" => error} = payload, socket),
    do: {:noreply, assign(socket, error: message(error, payload["exception"]))}

  def handle_event("ithibati:done", _payload, socket), do: {:noreply, socket}

  defp message("ceremony_failed", name) when is_binary(name), do: "Your browser refused: #{name}."
  defp message(error, _name), do: message(error)

  defp message("username_taken"), do: "That username is taken."
  defp message("invalid_username"),
    do: "A username is letters, digits and underscores, up to thirty characters."
  defp message("username_required"), do: "Pick a username to register."
  defp message("no_credentials"), do: "No passkey is registered here yet."
  defp message("invalid_code"), do: "That recovery code is not one we can use."
  defp message("ceremony_cancelled"), do: "The passkey prompt was dismissed."
  defp message("already_enrolled"), do: "That device already holds a passkey for this site."
  defp message("no_challenge"), do: "That took too long. Start again."
  defp message("malformed_credential"), do: "Your browser sent something this site cannot read."
  defp message("not_discoverable"), do: "That device will not store a passkey this site can find."
  defp message("unknown_credential"), do: "That passkey is not one this site knows."
  defp message("no_attested_credential"), do: "Your browser sent no passkey to store."
  defp message("credential_id_too_long"), do: "That passkey is bigger than this site can store."
  defp message("verification_failed"), do: "That did not check out. Start again."
  defp message("ceremony_failed"), do: "Your browser stopped partway through."
  defp message("recovery_failed"), do: "Recovery did not finish. Please try again."
  defp message("unknown"), do: "That request failed without saying why."
  defp message(other), do: "Something went wrong: #{other}"

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash}>
      <div :if={@error} class="alert alert-error"><span>{@error}</span></div>

      <form phx-change="validate" phx-submit="register">
        <.input name="username" value={@username} label="Username" required placeholder="ada_lovelace" />
        <.button variant="primary">Register</.button>
      </form>

      <.button phx-click="sign-in" class="btn mt-4">Sign in with a passkey</.button>

      <form phx-submit="recover" class="mt-8">
        <.input name="code" value="" label="Lost your passkey? Use a recovery code" required />
        <.button class="btn">Sign in with a code</.button>
      </form>

      <div
        id="passkey"
        phx-hook="Ithibati.Web.Hooks.PasskeyCeremony"
        data-registration-challenge-url={~p"/auth/registration/challenge"}
        data-registration-url={~p"/auth/registration"}
        data-authentication-challenge-url={~p"/auth/authentication/challenge"}
        data-authentication-url={~p"/auth/authentication"}
        data-recovery-url={~p"/auth/recovery"}
      >
      </div>
    </Layouts.app>
    """
  end
end
```

The hook reads its endpoint URLs from the empty `div`. `push_event/3` starts the exchange;
`ithibati:failed` returns an error code for your page to explain. The `validate` event in this
example only retains the username; the handler performs validation before issuing a challenge.

Create the protected page at `lib/my_app_web/live/inside_live.ex`:

```elixir
defmodule MyAppWeb.InsideLive do
  use MyAppWeb, :live_view

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash}>
      <p>
        Signed in as <strong>{@current_account.username}</strong>
        — <.link href={~p"/session"} method="delete" class="link">sign out</.link>.
      </p>
    </Layouts.app>
    """
  end
end
```

## 10. Check it, then run it

```console
$ mix ithibati.doctor
$ mix phx.server
```

Open `http://localhost:4000` and try the complete flow:

1. Register a username and passkey.
2. Save the recovery codes and follow **Done** to `/inside`.
3. Sign out, then open `/inside` directly. It should redirect to `/`.
4. Sign in with the passkey. You should return to `/inside`.
5. Sign out and sign in with one recovery code.
6. Sign out and try that same code again. It should be refused.

The doctor checks setup; this browser pass checks the pages, assets and redirects as well.
See [Setup checks](doctor.md) if configuration or migrations fail.

Before deploying, set the endpoint's public URL as described in
[The relying party](ceremonies.md#the-relying-party), and add application-level rate limiting
to sign-in and recovery requests.

## Where to go next

- [Configuration and schemas](configuration.md): identifiers, indexes, table names and migration upgrades.
- [Registering and signing in](ceremonies.md): callbacks, browser events, sessions and direct ceremonies.
- [Passkeys](passkeys.md): enrol and manage additional credentials.
- [Recovery codes](recovery.md): issue, redeem and replace codes.
- [Invitations and the first account](invitations.md): restrict who can register.
