# Passkeys — sign in with the device, no password and no code

> Every app on the UserAccounts kit signs people in with passkeys: a key pair their phone, laptop or security key makes for the app and unlocks with a PIN, a fingerprint or a face. A passkey is both steps at once, so it needs no password and no code, and a look-alike page cannot collect it. An account may hold several, name and remove them, and turn password sign-in off once it has one; a lost device is recovered by another passkey or by the reset link.

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

## Summary        {#summary}
`use Osysharp.UserAccounts;` gives an app passkeys with nothing more to write: "Sign in with a passkey" on the sign-in
page wherever the browser can make one, a **Passkeys** section on the account page (add, name, remove), "Set up a
passkey instead" for an account the app requires two-step sign-in of, and "Turn off password sign-in" once an account
has a passkey. The flows are ordinary functions, so an app with a sign-in page of its own calls them itself.

## Signature      {#signature}
```osy syntax
StartPasskeySignIn() -> PasskeyStart                                  // { Token, Options } for Passkey.Get
SignInWithPasskey(<string> token, <string> answer) -> SignInStep      // a ticket — or MustVerify
AddPasskey(<string> email, <string> password = "", <string> code = "", <string> passkeyToken = "",
           <string> passkeyResponse = "", <string> enrolment = "", <string> addToken = "",
           <string> addResponse = "", <string> name = "") -> PasskeyAdding
RemovePasskey(<string> email, <string> credentialId, <string> password = "", <string> code = "",
              <string> passkeyToken = "", <string> passkeyResponse = "") -> string
RemovePassword(<string> email, <string> passkeyToken, <string> passkeyResponse) -> string   // a fresh ticket
RenamePasskey(<string> credentialId, <string> name)                   // as the signed-in account
MyPasskeys() -> PasskeysFacts                                         // { Passkeys, PasswordOff }
```

## Description    {#description}

### Why is a passkey both steps at once?   {#both-steps}
A passkey is something the person HAS (the device holding the private key, which never leaves it) and something they
ARE or KNOW (what unlocks it). The kit asks the device to check the person every time — user verification is
required, not preferred — and refuses an answer that says it did not. So a passkey sign-in answers a ticket with no
code after it, even for an account the app requires two-step sign-in of (`AccountsSetup.SecondFactor`): it replaces the
password and the authenticator app together.

### How does a sign-in go?   {#sign-in}
`StartPasskeySignIn()` mints a challenge and answers the options for the browser; `Passkey.Get(options)` asks the
device, which offers every passkey it holds for the app — nobody types an address — and answers what it signed, or ""
when the person closed the prompt; `SignInWithPasskey(token, answer)` checks it and answers the step: a ticket for
`Session.SignIn`, or `MustVerify` when the app requires a proven address and this one is not.

### How does an account add one?   {#add}
In two halves, as the browser needs. The first `AddPasskey` call takes proof that it is the account's owner — the
password (and a code, with two-step sign-in on), or one of its passkeys (`passkeyToken` + `passkeyResponse`, a
confirmation `StartPasskeySignIn` began), or the `enrolment` token a sign-in that answered `MustEnrol` carried — and
answers the options for `Passkey.Create`. The second call, with `addToken` and the browser's answer, keeps it under
`name` and mails the owner. For an enrolment it also answers the ticket: the passkey IS the second step that sign-in
was waiting for.

### What may never be removed?   {#last-way-in}
The last way in. A passkey is removed only while the account keeps a password, another passkey or a linked provider,
and the password only while it keeps a passkey. Removing takes the same proof adding does.

### What does turning password sign-in off do?   {#password-off}
`RemovePassword` REMOVES the password — confirmed with one of the account's passkeys, which proves at the same moment
that the account can sign in without it. With no password stored, nothing that knows only passwords can sign the
account in: the app's own sign-in, the command line's, an operator's import. Every other session ends. `MyPasskeys()`
says `PasswordOff`, and the account page says so in the Password section.

### What if every device is lost?   {#recovery}
Another passkey signs in — which is why an account may hold several (the Mac, the phone, a hardware key in a drawer).
With none left, "Forgot your password?" mails a link that sets a password again and turns password sign-in back on,
told by mail; the account then signs in as usual, with its second step if it has one.

### What holds?   {#guarantees}
- **Every answer is checked against a challenge the sign-in flow minted**, kept as its hash, alive five minutes, and
  spent by the first answer that comes back, right or wrong; an add's challenge is never a sign-in's.
- **The browser's answer must be for this app** — the origin and the host of `App.BaseUrl` — **and the device must
  have checked the person.**
- **A signature counter that does not move forward is refused**; synced passkeys report zero every time, which is not
  a regression.
- **A passkey is one account's**: its credential id is unique in the app, and its user handle must be its account's.
- **Only the sign-in flow writes a passkey's key, id, account and counter**; its owner may rename it and read only
  its own.
- **A stranger who signed up first with somebody's address loses their passkey** the moment the owner proves the
  address, with everything else they set up.

### Where does it work?   {#where}
Wherever the page is a secure context — `https://`, or `http://localhost` and its subdomains in development — on the
app's own address. A passkey is made for the host it was made on, so one made on `acme.osyrin.app` is not offered on
`help.acme.com`. `Passkey.Supported()` is false where it cannot work, and the kit's buttons hide.

## Examples       {#examples}

### A sign-in button of your own   {#example-button}
```osy title="an app's own page, signing in with a passkey through the kit's flows" test app=security-passkeys
app Desk {
  use Osysharp.Ui;
  use Osysharp.Identity;
  use Osysharp.Mail;
  use Osysharp.UserAccounts;
  use Osysharp.EmailTemplates;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.UserAccounts;

[Role] enum AppRole { Authenticator, Member }

app.Accounts = new AccountsSetup { AppName = "Harbour Desk", SignupRole = AppRole.Member };

[Route("/quick")] [AllowAnonymous] [Render(CSR)]
component QuickSignIn() {
  bool supported = false;
  string problem = "";
  on mount { supported = Passkey.Supported(); }

  action UsePasskey() {
    problem = "";
    try {
      var start = StartPasskeySignIn();
      var answer = Passkey.Get(start.Options);
      if (answer == "") { return; }            // the person closed the prompt
      var step = SignInWithPasskey(start.Token, answer);
      if (step.Ticket != "") { Session.SignIn(step.Ticket, "/"); }
    }
    catch (Exception ex) { problem = ex.Message; }
  }

  render {
    Stack(gap: 3) {
      if (supported) { Button("Sign in with a passkey", onPress: UsePasskey); }
      else { Text("This browser cannot use passkeys."); }
      if (problem != "") { Text(problem); }
    }
  }
}

[Route("/")] component Home() { render { Text("The desk"); } }
```

## See also       {#see-also}
- [The UserAccounts kit — sign-in done properly, once](https://osysharp.com/reference/security/accounts-kit/) — the kit, and the rest of its sign-in
- [WebAuthn.* and Passkey.* — the two halves of a passkey](https://osysharp.com/reference/function/webauthn/) — the checks on the browser's answer, and `Passkey.Create`/`Passkey.Get`
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — the five slots a passkey's flows are armed through
- [Totp.NewSecret, Totp.Uri, Totp.Verify, Totp.Code](https://osysharp.com/reference/function/totp/) — the authenticator-app second step a passkey can stand in for
