Ithibati

Ithibati

Passkey authentication for Elixir applications: accounts, WebAuthn credentials, recovery codes and revocable sessions. It has no opinion about what an account may do. The web half is optional and built for Phoenix.

Swahili, ithibati: proof, evidence. In WebAuthn's own vocabulary, attestation.

What it is

Most authentication libraries answer two questions at once. Who is this, and what may they do.

The second answer is different in every application. Sites, tenants, teams, roles, invitations. That is why the first one so rarely gets reused. Ithibati answers only the first:

You keep your own users table and your own roles, and Ithibati never reads them. Creating an account, its first passkey and its recovery codes is one transaction, and you add your own steps to it.

An account is identified by a username or an email address, whichever your application uses. Two patterns ship, Identifier.username_format/0 and Identifier.email_format/0. You can pass your own. You declare the field on your own schema.

Open registration and invitation-only both work. Ithibati does not push you either way.

It sends no mail, so delivering an invitation link is your job. An invitation token is a bearer secret, and whoever has the link accepts it. That also answers address verification. An application that mails the link itself has proved the address, because Ithibati refuses an account created under any identifier but the one the invitation was addressed to.

What using it looks like

There are five touchpoints, and they are the whole surface. Getting started builds them into a working application, step by step, from mix phx.new onwards.

The dependency:

{:ithibati, "~> 0.1"}

One line in your account schema names the field an account is known by, and brings the changeset that validates it:

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

One line in your router mounts the ceremony endpoints:

ithibati_routes handler: MyAppWeb.Auth, rp_name: "MyApp"

Your handler decides what a verified assertion is worth. Ithibati hands you the account and issues nothing:

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

And a page starts a ceremony by pushing an event to the browser hook:

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

There is no password field. There is also no sign-in form. A sign-in challenge names no credential, so the browser offers whichever passkeys it holds for your site and the person picks one. Registration still needs a form, because the name does not exist yet.

Configuration, the migration and the account schema in full are in the guide. mix ithibati.doctor checks the lot when you are done.

Without Phoenix

Phoenix, LiveView and Plug are optional dependencies. The web half comes with them: the ceremony routes, a gate that answers who is signed in, and a browser hook. Most applications want that, and both examples are built that way.

You can skip it. A ceremony is a handful of function calls. The controller only adds HTTP and somewhere to park the challenge between two requests. The browser side works without a framework too. priv/static/ithibati.js exports register and authenticate as plain functions, and the LiveView hook wraps them. Use this for a native client, a browser extension, or a server that is not Phoenix. Registering and signing in shows both ways.

Where to go next

The documentation covers it properly. Getting started, registering and signing in, recovery codes, passkeys, invitations and the first account and the tooling.

Two example applications compile and run, one for each way of letting people in. Read these when the docs and your editor disagree:

CI compiles both, runs their tests and drives them in a browser, so they cannot quietly rot.

CHANGELOG.md has what changed between releases.

Licence

MIT. See LICENSE.