# WebAuthn.* and Passkey.* — the two halves of a passkey

> A passkey (W3C Web Authentication) has a server half and a browser half. `WebAuthn.*` runs on the server: it makes the options a browser is handed and checks what comes back — CBOR, COSE keys, ES256/RS256/EdDSA signatures, the origin, the relying party, user verification and the signature counter. `Passkey.Create`/`Passkey.Get` run in the browser and ask the person's device. `WebAuthn.NewTestKey`/`TestCreate`/`TestGet` are the device's half, for tests.

<!-- id: function-webauthn · area: function · stability: preview · html: https://osysharp.com/reference/function/webauthn/ -->

## Summary        {#summary}
If what you want is passkey sign-in, you do not need these directly — [the UserAccounts kit](https://osysharp.com/reference/security/passkeys/) has it,
pages and all. These are what it is built on, for a sign-in of your own:

- `WebAuthn.CreationOptions(…)` / `WebAuthn.RequestOptions(…)` — the options for the browser, carrying your challenge.
- `Passkey.Create(options)` / `Passkey.Get(options)` — the browser asks the device; the answer is WebAuthn's own JSON,
  or "" when the person closed the prompt.
- `WebAuthn.Register(…)` — check a new passkey and answer what to keep; `WebAuthn.Verify(…)` — check a sign-in and
  answer the new counter. Both throw a `ValidationException` saying what failed.

## Signature      {#signature}
```osy syntax
WebAuthn.CreationOptions(<string> challenge, <string> rpId, <string> rpName, <string> userHandle, <string> userName,
                         <string> displayName, <string[]> exclude) -> string
WebAuthn.RequestOptions(<string> challenge, <string> rpId, <string[]> allow) -> string
WebAuthn.Register(<string> response, <string> challenge, <string> origin, <string> rpId) -> string   // JSON
WebAuthn.CredentialId(<string> response) -> string
WebAuthn.Verify(<string> response, <string> publicKey, <long> storedSignCount, <string> challenge, <string> origin,
                <string> rpId, <string> userHandle) -> long
Passkey.Create(<string> options) -> string      // in the browser
Passkey.Get(<string> options) -> string         // in the browser
Passkey.Supported() -> bool                     // in the browser
WebAuthn.NewTestKey() -> string                 // the device's half, for tests
WebAuthn.TestCredentialId(<string> key) -> string
WebAuthn.TestCreate(<string> key, <string> options, <string> origin) -> string
WebAuthn.TestGet(<string> key, <string> options, <string> origin, <string> userHandle, <long> signCount,
                 <bool> userVerified) -> string
```

## Description    {#description}

### What do the options ask for?   {#options}
`CreationOptions` asks for a **discoverable** credential (it signs in with no address typed), **user verification
required**, ES256, EdDSA or RS256, no attestation, and excludes the `exclude` credential ids so one device is not
registered twice. `RequestOptions` with an empty `allow` lets the browser offer every passkey it holds for the app; with
an account's ids it asks for one of those. The challenge you pass is what the browser signs — mint it per ceremony,
keep its hash, and spend it on the first answer.

### What does Register answer?   {#register}
JSON — `{ "CredentialId", "PublicKey", "SignCount", "BackedUp", "Algorithm" }` — which you deserialize into a class of
your own and keep: the public key is not a secret; the credential id is how a sign-in finds it. Attestation is not
checked: there is no trust anchor to check it against when any make of authenticator is welcome.

### What does Verify check?   {#verify}
That the client data is a `webauthn.get` for THIS challenge from THIS origin, not framed by another site; that the
authenticator data is for THIS relying party with the person present AND verified; that the user handle, when there is
one, is `userHandle`; that the signature verifies under the stored key; and that the counter moved forward when either
it or the stored one is not zero (a synced passkey reports zero every time). It answers the new counter to store.

`origin` is the page's scheme, host and port (`https://acme.osyrin.app`); `rpId` is the host alone. Read both off the
app's own address — `new Uri(App.BaseUrl)` — never off anything the browser sends.

### Why does a closed prompt answer ""?   {#cancel}
The browser reports a dismissed prompt and a timed-out one alike, on purpose, and neither is a fault: nothing happened.
Every other refusal — a page not on the app's own address, a device already holding a passkey for the account — is a
`DeviceUnavailableException` with the browser's reason.

### How does a test play the device?   {#testing}
`WebAuthn.NewTestKey()` makes a P-256 key held as text; `TestCreate(key, options, origin)` and `TestGet(key, options,
origin, userHandle, signCount, userVerified)` answer exactly what a browser's `create` and `get` would — real CBOR, real
signatures — so the same `Register` and `Verify` check them. Pass another origin, a counter that goes back, or
`userVerified: false` to watch each refusal. `TestCredentialId(key)` is the id `TestCreate` gives the passkey.

### Where does each run?   {#sides}
`WebAuthn.*` is server-only: a page that checked its own authenticator's answer would prove nothing to anybody.
`Passkey.*` is browser-only: the device is reached through `navigator.credentials`.

## Examples       {#examples}
```osy title="the server half of a sign-in of your own" test app=function-webauthn
entity Key {
  [Required, Unique, MaxLength(1400)] string CredentialId;
  [Required, MaxLength(2000)] string PublicKey;
  long SignCount = 0;
  security { allow read, create, update when IsAuthenticated; }
}

class Kept { public string CredentialId = ""; public string PublicKey = ""; public long SignCount = 0; }

string Origin() { var u = new Uri(App.BaseUrl); return u.Scheme + "://" + u.Authority; }
string Site() { return new Uri(App.BaseUrl).Host; }

/// Keep the passkey `answer` registered for `challenge`.
void Keep(string answer, string challenge) {
  var kept = JsonSerializer.Deserialize<Kept>(WebAuthn.Register(answer, challenge, Origin(), Site()));
  new Key { CredentialId = kept.CredentialId, PublicKey = kept.PublicKey, SignCount = kept.SignCount };
}

/// Is `answer` a good sign-in for `challenge` by the account whose handle is `handle`?
bool Check(string answer, string challenge, string handle) {
  var id = WebAuthn.CredentialId(answer);
  var key = Key.FirstOrDefault(k => k.CredentialId == id);
  if (key == null) { return false; }
  key.SignCount = WebAuthn.Verify(answer, key.PublicKey, key.SignCount, challenge, Origin(), Site(), handle);
  return true;
}
```

## See also       {#see-also}
- [Passkeys — sign in with the device, no password and no code](https://osysharp.com/reference/security/passkeys/) — passkeys in every app on the UserAccounts kit
- [The UserAccounts kit — sign-in done properly, once](https://osysharp.com/reference/security/accounts-kit/) — the kit
- [Totp.NewSecret, Totp.Uri, Totp.Verify, Totp.Code](https://osysharp.com/reference/function/totp/) — the authenticator-app second step
- [Security.* — hashing, verifying, tickets, random ids](https://osysharp.com/reference/stdlib/security/) — hashing, random ids and the rest of the credential surface
