# The UserAccounts kit — sign-in done properly, once

> `Osysharp.UserAccounts` is the default way into an app: sign-in with a password or a provider (Google, GitHub, …), sign-up (which an app can close), a forgotten password, a changed password or address, address verification, linking a provider to an account, and two-step sign-in with an authenticator app — with the screens for all of them in the app's theme and its mail through Osysharp.Mail. An address is proven by a mailed link or a six-digit code; a phone number, when the app keeps them, by a texted one. The app writes its roles and one setting; every credential rule holds by construction, on `User` and on any type an app derives from it.

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

## Summary        {#summary}
An address and a password, done properly once: `use Osysharp.UserAccounts@1;` gives an app sign-in, sign-up, a forgotten
password, a changed password or address, address verification, two-step sign-in with an authenticator app, and a screen
for each — and wires them into the app's sign-in for it. The app writes **its roles** and **one setting**:

```osy syntax
[Role] enum AppRole { Authenticator, Agent, Lead }          // `Authenticator` is the role the sign-in flow runs as
app.Accounts = new AccountsSetup { AppName = "Acme Support", SignupRole = AppRole.Agent };
```

The kit's pages are served at the kit's routes — the app writes none of them ([Pages](#pages)). With `app.OAuthClients`
declared, the same pages sign people in with Google or GitHub, and link either to an account ([Providers](#providers)).

## Signature      {#signature}
```osy syntax
use Osysharp.UserAccounts@1;           // with Osysharp.Identity@0, Osysharp.Mail@0, Osysharp.Sms@0, Osysharp.EmailTemplates@0, Osysharp.Ui
                                   // (it brings Osysharp.Workflow itself — an app writes that `use` only once it names a workflow)

[Role] enum AppRole { Authenticator, … }

app.Accounts = new AccountsSetup {
  AppName              = "…",          // on the screens and in the mail
  SignupOpen           = true,         // false: accounts come from an operator (`osy user add`) or an invitation
  SignupRole           = AppRole.…,    // the role a new account gets; none by default
  FirstAccountRole     = AppRole.…,    // the role the FIRST account also gets — whoever sets the app up runs it
  MinPasswordLength    = 10,
  RequireVerifiedEmail = false,        // true: no ticket until the address is proven — and sign-up never says who has an account
  AddressProof         = new AddressProofPolicy(),   // derive to say whose address must be proven first when not everybody's
  HomePath             = "/",
  Mails                = new AccountMails(),    // derive to write your own mail
  Texts                = new AccountTexts(),    // derive to write your own text messages
  ProvePhones          = false,        // accounts may add a phone number, proven by a texted code
  SmsSignIn            = false,        // …and sign in with a code texted to it
  SmsSecondStep        = false,        // …and choose a texted code as their second step (never one an app can REQUIRE)
  Kinds                = new AccountKinds(),    // derive to make your own kind of account on sign-up
  SecondFactor         = new SecondFactorPolicy(),   // derive to say who MUST use two-step sign-in
  Events               = new AccountEvents(),   // derive to hear of every sign-in (a sign-in history, say)
  OwnDoor              = new OwnDoorPolicy(),   // derive to send a provider sign-in back to the app's own door
  LogoUrl              = "",           // the app's mark on every sign-in screen — File.Url(…) or https
};
```

Provider sign-in needs only what an app declares for OAuth anyway — its clients (`app.OAuthClients`, capability
`Login`) and `use Osysharp.Oauth;` — and the kit offers each as "Continue with …" ([Signing in with a provider](#providers)).

## Description    {#description}

### What the kit does                          {#what}
| Flow | Function (an `[AuthMethod]`, wired for you) | Page (served for you) | Route |
|---|---|---|---|
| Sign in | `SignIn(email, password)` | `SignInPage` | `/login` |
| Sign up | `SignUp(email, password, name)` | `SignUpPage` | `/sign-up` |
| Ask for a reset link | `RequestPasswordReset(email)` | `ForgotPasswordPage` | `/forgot-password` |
| Choose a new password from the link | `ResetPassword(token, password)` | `ResetPasswordPage` | `/reset-password/{token}` |
| Change the password (signed in) | `ChangePassword(email, current, new)` | `AccountPage` | `/account` |
| Change the address (signed in) | `ChangeEmail(email, current, newEmail)` | `AccountPage` | `/account` |
| Call off a move it did not ask for (from the OLD address's mail) | `CancelAddressChange(token)` | `CancelAddressChangePage` | `/cancel-address-change/{token}` |
| Call off its own pending move (signed in) | `CancelMyAddressChange()` | `AccountPage` | `/account` |
| Open a verification link | `VerifyEmail(token, password, newPassword)` | `VerifyEmailPage` | `/verify-email/{token}` |
| Type the verification mail's code | `VerifyEmailCode(email, code, password, newPassword)` | `SignUpPage` · `SignInPage` (when the address must be proven) | `/sign-up` · `/login` |
| Add or change a phone number | `ProvePhone(email, password, phone, code)` | `AccountPage` (with `ProvePhones`) | `/account` |
| Sign in with a texted code | `SignInWithPhone(phone, code)` | `SignInPage` (with `SmsSignIn`) | `/login` |
| A texted code as the second step | `SetSmsSecondStep(email, password, on, code)` | `AccountPage` (with `SmsSecondStep`) | `/account` |
| The code after the password | `SignInWithCode(challenge, code)` | `SignInPage` (its second step) | `/login` |
| Turn two-step sign-in on | `EnableSecondFactor(email, password, key, code)` | `SignInPage` (when required) · `AccountPage` | `/login` · `/account` |
| Turn it off | `DisableSecondFactor(email, password, code)` | `AccountPage` | `/account` |
| New recovery codes | `NewRecoveryCodes(email, password, code)` | `AccountPage` | `/account` |
| Ask for a new verification link | `RequestVerification(email)` | `AccountPage` (a changed address) · your operator page | — |
| Sign in with a provider | `SignInWithProvider(pendingToken)` | `ProviderSignInPage` | `/oauth/sign-in` |
| Link a provider to the account | `LinkProvider(email, password, pendingToken, code)` | `ProviderSignInPage` (signed in) | `/oauth/sign-in` |
| Unlink one | `UnlinkProvider(email, password, provider, code)` | `AccountPage` | `/account` |

`use Osysharp.UserAccounts;` states `app.AuthBootstrap` and `app.Auth` for the app ([auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/)), so every
flow above is reachable by a signed-out visitor and `osy user add` works — an app that states its own
`app.AuthBootstrap` replaces the kit's whole.

**A flow your own bootstrap leaves out is named.** Because yours replaces the kit's whole, a slot you do not write is a
flow nobody wires — its page looks live and every visitor is refused. So the compile warns `AUTH_KIT_FLOW_UNWIRED` once
for each such slot, naming the kit's function, and the app still compiles. Wire it, or write the slot as `null` to say
you leave it out on purpose; a `null` slot wires nothing and is not warned about (`Login` is never optional):

```osy title="an app's own sign-in wiring that leaves the kit's sign-up out on purpose" syntax
app.AuthBootstrap = new AuthBootstrap {
  Role = AppRole.Authenticator,
  Login = SignIn, PasswordResetRequest = RequestPasswordReset, PasswordReset = ResetPassword,
  // … every other kit flow you keep …
  Signup = null,   // the only way in is our own invitation, so the kit's sign-up stays unwired
};
```

### What holds by construction                 {#guarantees}
- **The hash is the sign-in flow's alone.** `deny read PasswordHash when !IsAuthenticator` and the same for writes, on
  `User` — and, because a package's rules are the floor every type derived from it stands on, on an app's
  `Employee : User` too. A signed-in person never reads a hash, their own included.
- **An unknown address costs a hash.** Every refusal from `SignIn` spends one password verification and answers the
  same `""`, so neither the answer nor its timing says which addresses have accounts.
- **Wrong passwords are limited; right ones are not.** The platform's
  [[security-auth-bootstrap#sign-in-limits|sign-in limits]] hold for `SignIn`, `ChangePassword`, `ChangeEmail` and every other flow here that checks a password: ten
  failures a minute from one address, and a growing wait (never a lock) for an address guessed at from anywhere. A right
  password answered with a two-step challenge counts as a success; the code step has its own limit, below.
- **A reset link is a mailed secret, stored hashed, working once, for an hour.** Pressing the button twice mails once.
  Using one link spends every other outstanding one.
- **An address is proven by a message it received** — its link, or the six-digit code beside it. Only token hashes and
  keyed code fingerprints are stored and only the sign-in flow writes them, so no account can make one whose secret it
  knows. A change of address takes effect only from the NEW address's link or code; the OLD address is told — and its
  mail carries a single-use "This wasn't me — cancel the change" link that calls the move off with no password (the
  asker has it): the new address's link and code then confirm nothing, and every session of the account ends.
- **Every one-time code is bounded three ways** — a keyed fingerprint (never the code), fifteen minutes, dead at its
  fifth wrong try — and each wrong code counts against the account's wrong-code limit and the sign-in limits. See
  [below](#codes).
- **With `RequireVerifiedEmail`, no ticket before the address is proven** — sign-up and sign-in answer `MustVerify`. An
  app that wants it of SOME people (the public, while its own team signs in at once) derives `AddressProofPolicy` and
  overrides `Required(User account)`, exactly as `SecondFactorPolicy` says who must use two-step sign-in.
- **Signing up first with somebody else's address gets you nothing.** An account's first proof needs the password it
  was made with, or hands the account to whoever holds the address. See [below](#first-proof).
- **Sign-up never says whether an address has an account** — the mail does. See [below](#sign-up-answer).
- **A texted code is never a second factor an app can require.** See [below](#phones).
- **One address, one account** — `[Unique]` across every kind of account, stored trimmed and lower-case.
- **Sign-up grants only what the app chose** (`SignupRole`, `FirstAccountRole`) — never a role the visitor names.
  `SignupRole` is an app-wide role every stranger can get, so it must never be one that manages people or
  organisations: an organisation's owner is a role held IN it ([[security-roles-held-in#signup]]), and `osy lint`
  refuses the other shape (`security-signup-role-administers-everything`, MUST).
- **A password change needs the current password**, so a session left open cannot be turned into a new password; the
  account is told by mail.
- **A password change, a reset or a claim signs the account out everywhere else.** See [below](#sessions).
- **The hash never reaches the audit trail** — the platform redacts the field `app.Auth` names.
- **With two-step sign-in on, a password alone signs nobody in** — not the sign-in page, not a mailed reset link, not
  a provider, not the platform's own console sign-in. See [below](#two-step).
- **A provider is matched to an account only through an address the account has PROVEN** — never by an address alone.
  See [below](#providers).

### How is an address proven — link or code?      {#codes}
Every verification mail carries **both**: a link (it works for three days) and a six-digit code (fifteen minutes).
Either one proves the address and spends the other. The code is for the person still on the page — the kit's sign-up
and sign-in screens ask for it when the app requires a proven address — and `VerifyEmailCode(email, code)` answers the
step a sign-in would: a ticket, a code from an authenticator, or setting one up. Receiving the mail is proof enough to
sign in, exactly as a reset link is.

A code is kept only as a keyed fingerprint with a nonce of its own (a stolen table cannot be guessed offline), a newer
code for the same thing spends the older, a code dies at its fifth wrong try, and each wrong try counts against the
account's wrong-code limit (the one authenticator codes spend: ten in fifteen minutes and no code is checked) — and,
because a wrong code throws inside an `[AuthMethod]`, against the [[security-auth-bootstrap#sign-in-limits|sign-in
limits]] too. Texted codes ([below](#phones)) are the same mechanism on the other channel.

### Can the desk change somebody's address for them?   {#assisted-change}
Yes — at once, and on the record. A person whose old address is gone cannot open a link sent there, and the desk answers
for the business, so a person the app names calls `ChangeAddressFor(account, newAddress, reason)` with how they know it
is that person's account ("ID checked at the desk"). The account moves when the change commits. An
`AddressChangeRequest` keeps the old address, the new one, the reason, who made it (`[CreatedBy] MadeBy`) and when; the
OLD address is told it moved (`AccountMails.AddressChangedFor`) and the NEW one is welcomed (`AddressMovedHere`) —
neither mail asks for anything. A change the person had asked for themselves, to some other address, is spent.

A change a person makes **themselves** is different and stays so: `ChangeEmail` needs their password (and their second
step), mails a link and a code to the new address, tells the old one, and moves nothing until the new address confirms
it — an address is where a password is reset to, so whoever knows the password must not be able to move it alone.

Who may make a desk change is the app's to say, and nobody may until it does — `ChangesAccountsFor` is a
`virtual policy` whose default is `false`:

```osy title="the leads may move an account for somebody" syntax
policy ChangesAccountsFor => IsLead;
```

A change is made only by that function (`deny create except via ChangeAddressFor` — [deny … except via](https://osysharp.com/reference/security/except-via/)), which
checks first: an address that is not one, the account's own, or another account's, and a change with no reason, are
refused with the sentence to show. The desk's own context never writes the address; the change's run does.

### An account for somebody who never signs in   {#managed}
A child a parent books for, a walk-in the desk records, a member a coach keeps a register of: they need an account —
bookings, passes and signatures point at one — and will not sign in. `MakeManagedAccount(name)` makes it: the app's own
kind of account (`AccountKinds.New`), named as given, with no password and an address on a domain that cannot receive mail
(`…@managed.invalid`), so nobody can sign in with it and no mail is ever sent to it. Its record
(`ManagedAccountRequest`) keeps the name, who made it (`MadeBy`) and when, and who looks after it — the caller, unless
`lookAfterIt: false` (a walk-in). `ManagedAccountsOf(guardian)` lists the accounts a person looks after.

Who may make one is the app's to say, and nobody may until it does — `MakesManagedAccounts` is a `virtual policy` whose
default is `false`:

```osy title="members may add the family members they book for" syntax
policy MakesManagedAccounts => IsMember;
```

Later the person can have a login of their own: their guardian (or, for an account made with none, whoever may make
them) calls `GiveManagedAccountAnAddress(account, address)`. A link goes to that address; the account moves there only
when it is opened — the same confirmation every address change takes — and the person then chooses a password with
"forgot password". The record is never written directly (`deny create except via MakeManagedAccount`); with
Osysharp.Privacy, the managed account is in its guardian's copy of their data and is erased with the person it was made
for (Osysharp.Privacy.UserAccounts).

### Who can change an account's address?   {#address-writes}
Nobody, directly — not the account itself, not the app's staff, not a client's unit of work or an API call, and not even
the sign-in flow outside one function. `User` carries

```osy syntax
deny update Email except via ConfirmAddressChange;
```

on the address it inherits from Osysharp.Identity's `Principal` ([deny on an inherited field](https://osysharp.com/reference/security/inherited-field-deny/)), so the address moves
in exactly two places: `ConfirmAddressChange`, which runs when the NEW address has opened its link or typed its code
(`VerifyEmail` / `VerifyEmailCode` on a change), and the desk's recorded change above (its run is the kit's own code).
Anything else that writes `Email` on an account — a page that sets it and commits, a request that sends it — is refused,
and the address stays.

### What if somebody signed up with my address before I did?   {#first-proof}
That is **account pre-hijacking**: a stranger makes an account with your address and a password of their own, and waits
for you to confirm the address. The kit closes it at that moment — **the FIRST time an account's own address is proven,
on an account that already has a password**:

- **The link or code alone proves nothing yet.** `VerifyEmail(token)` / `VerifyEmailCode(email, code)` answer
  `MustConfirmPassword` — nothing proven, nothing spent — and the page asks for one of two things.
- **The password the account was made with** (`VerifyEmail(token, password)`): whoever made it is the one proving it —
  proven, and signed in as a sign-in would be (a code from their authenticator next, if they set one up).
- **Or a new password** (`VerifyEmail(token, "", newPassword)`): whoever holds the address takes the account. Every way
  in set up before the proof is cleared — the old password, two-step sign-in and its recovery codes, a phone and its
  texted step, every linked provider, every outstanding reset link and challenge — then the new password is set and
  they are signed in.

**The person who signed up is not asked twice.** The discriminator is the password, not the mailbox: the mail proves
who HOLDS the address, only the password says whether that is who CHOSE it. So the kit's sign-up and sign-in screens
send the password they just took along with the code (`AddressCodeStep(address, then, password: …)`) and the maker goes
straight in; only the link page, opened on its own, asks for it — the same password they would type to sign in
afterwards anyway. A reset link that is an account's first proof (`ResetPassword`) clears the same things, since it
chooses a new password by definition.

What the stranger did while signed in — with `RequireVerifiedEmail` off they were — stays in the account's data; the
ways in do not. Apps where that matters turn `RequireVerifiedEmail` on, so nobody is signed in before proving anything.

### What happens to the account's other sessions when its password changes?   {#sessions}
**They end.** An account's sessions are valid from its last change: `ChangePassword`, `ResetPassword`, the proof that
moves the account to a new address, and the first proof that claims an account ([above](#first-proof)) each call
[[stdlib-security#end-other-sessions|`Security.EndOtherSessions(account.Id)`]], so every ticket the account already
holds — on any browser, on any node — is refused on its next request with *"You were signed out because this account's
password was changed or reset in another session. Sign in again."*, and any live socket it holds closes.

- **The session making the change stays signed in.** Changing your password on your laptop signs your phone out, not
  the laptop.
- **A reset, made from a signed-out browser, signs every session out** — and the ticket the reset answers, issued after,
  is the one that works.
- **The stranger of a claimed account is signed out at the claim.** With `RequireVerifiedEmail` off they were signed in
  at sign-up, with a ticket good for a day; it stops working the moment the owner proves the address with a new
  password. The maker who proves it with their OWN password changes nothing about the way in, and stays signed in.
- **A new address signs the other browsers out** when it is proven — the moment the account's way in moves. Asking
  for the change signs nobody out; the old address keeps working until the new one's link or code is used. Calling it
  off from the OLD address's link signs every browser out — whoever asked is signed in somewhere, and knows the password.
- **A change that does not happen signs nobody out.** The sessions end when the change COMMITS: a wrong current
  password, or any refusal later in the same call, leaves every ticket working.

### What does sign-up answer for an address that already has an account?   {#sign-up-answer}
**Not "there is already an account"** — that sentence tells a stranger who has one. What happens depends on
`RequireVerifiedEmail`:

- **On:** a taken address is answered **exactly** as a new one is — `MustVerify`, after the same hashing time — and the
  MAIL says which: a new address is sent its code and link, a taken one is told *"you already have an account"*, with
  the way to sign in or choose a new password. Nothing on the page, in the answer or in its timing differs.
- **Off:** a new account is signed in at once, and a taken address cannot be — so there, whether an address was free is
  visible to whoever tries it, by construction; the kit does not pretend otherwise. What it does: the **right password**
  for the taken address signs its owner in (they signed up with their own account — the check is an ordinary password
  check, under the sign-in limits), and any other gets the same `MustVerify` step and the same mail to the owner. An app
  that must not show which addresses have accounts turns `RequireVerifiedEmail` on.

Either way the owner is mailed at most once a minute, and every sign-up that went through counts against the platform's
[[security-auth-bootstrap#sign-up-cap|sign-up cap]] — 30 an hour from one address.

### Can a phone number sign in, or be a second step?   {#phones}
When the app keeps phone numbers (`ProvePhones`, or either option below), an account adds one on the account page and
proves it with a texted code (`ProvePhone`); only the sign-in flow writes the number, so no account can claim one it does
not hold, and nobody reads another account's number. The texts go through [The Sms kit — send a text message, keep a record, pick a carrier](https://osysharp.com/reference/ui/sms-kit/) — with no sender, the codes are
held in its outbox and nothing is sent. Two uses, each off until the app turns it on:

- **`SmsSignIn`** — a proven number signs in with a texted code in place of the password (`SignInWithPhone`), answered
  alike whether or not the number has an account; texted at most once a minute and five times an hour. Then the same
  step as after a password: a code from an authenticator when that is on. Off by default because whoever takes over a
  number (a SIM swap, a port-out) could then sign in with it — choose it where that trade is right for the app's people.
  A number more than one account could sign in with signs in NONE of them: no code is sent — the phone is texted to
  sign in with an address and password instead — and the caller is answered exactly as for any other number.
- **`SmsSecondStep`** — an account may choose a texted code after its password (`SetSmsSecondStep`, which takes a code
  from the phone to turn off). Such an account is not signed in by the phone alone: one phone is one factor.

⛔ **A texted code is NOT a second factor an app can require.** `SecondFactor.Required` always means an authenticator
app: an account the app requires one of is taken through setting one up whether or not it chose texted codes, and
`SignInWithCode` refuses a texted code for it (NIST 800-63B lists SMS as "restricted").

### Which roles does an account get?           {#roles}
The roles are the app's own `[Role]` enum, and it must have an `Authenticator` member — the role the sign-in flow runs
as, which the kit names as `role.Authenticator` ([`role` — a package's name for the app's own roles](https://osysharp.com/reference/security/role-type/)). The kit keeps grants in
`Osysharp.Identity`'s `RoleGrant`; write the app's own role policies over it:

```osy syntax
policy IsLead => RoleGrant.Any(g => g.Member == user && g.Role == AppRole.Lead);
```

A new account gets the role sign-up gives it; everything after that — a manager giving staff their roles and taking
them back, with a record of who changed what — is **Osysharp.UserManagement**, the user manager. An app with more than one role
usually adds it (`osy compose` and `osy lock` say so): [The UserManagement kit — the user manager: who is in the app, what each may do, and who changed it](https://osysharp.com/reference/security/people-kit/).

### An app's own kind of account               {#kinds}
Derive from `User` for what the app knows about its people, and tell sign-up to make that type:

```osy syntax
entity Employee : User {
  Department Department = Department.Support;
  security {                                             // its own grants, inside the kit's floor
    allow read when IsAuthenticated || IsAuthenticator;
    allow create when IsAuthenticator;
    allow update when IsAuthenticated || IsAuthenticator where Id == user.Id;
    allow update when IsAuthenticator;
  }
}
public class StaffKinds : AccountKinds {
  public override User New(string email, string name) { return new Employee { Email = email }; }
}
app.Accounts = new AccountsSetup { …, Kinds = new StaffKinds() };
```

`user` is the base `Principal` in every rule; `user is Employee` tells the kinds apart.

### How does two-step sign-in work?           {#two-step}
After the password, a six-digit code from an authenticator app on the person's phone (Google Authenticator, Microsoft
Authenticator, 1Password — any app that speaks [TOTP](https://osysharp.com/reference/function/totp/)); or, when the phone is lost, one of ten
one-time recovery codes. Anybody may turn it on from the account page; an app says who **must**:

```osy syntax
public class AdminsUseCodes : SecondFactorPolicy {
  public override bool Required(User account) {
    return RoleGrant.Any(g => g.Member == account && (g.Role == AppRole.Admin || g.Role == AppRole.Lead));
  }
}
app.Accounts = new AccountsSetup { …, SecondFactor = new AdminsUseCodes() };
```

An account that must, and has not set it up, is taken through setting it up **at sign-in** (and at sign-up) — it is
given no ticket until it has, so there is no page it reaches first. The kit's sign-in page does all of this in place.

**What holds, by construction:**
- **No ticket before the second factor.** For an account with it on, `SignIn` answers a **challenge**: kept only as a
  hash, good for five minutes, used once, and dead after five wrong codes. `SignInWithCode(challenge, code)` issues the
  ticket. A mailed reset link sets the password and signs nobody in; the platform's password-only sign-in refuses the
  account ([`SecondFactorField`](https://osysharp.com/reference/security/password-auth/), which the kit declares).
- **A code works once.** The step of the last code accepted is kept, and a code for it or an earlier step is refused —
  so a code read over a shoulder is useless the moment it has been used.
- **Guessing is bounded.** Ten wrong codes on one account in fifteen minutes and no code is checked for it — the right
  one included — until they age out. A fresh challenge per guess does not escape it.
- **The secret is nobody's but the sign-in flow's** — the account's own included once it is on — and is stored sealed
  with the app's own key (`Crypto.Encrypt`), so a backup or an operator's query is not a factor either.
- **Recovery codes are kept as keyed fingerprints**, work once, and a new set spends the old one. Using one mails the
  owner, as does turning the factor on or off.
- **Every change takes the password AND a current code**: turning it off, new recovery codes — and, while it is on, a
  new password or address. A password alone never removes the factor that exists because a password is not enough.

**Setting it up at sign-in needs no second password.** `MustEnrol` comes with an `Enrolment` token — kept as a hash,
good for fifteen minutes, used once, refused as a code challenge — that `EnableSecondFactor(…, enrolment: token)` takes
in place of the password. That is what lets an account that signed in with a PROVIDER, which typed no password, set the
factor up; the kit's screens pass it for you.

**A lost phone is an administrator's job.** `ClearSecondFactor(account)` turns the factor off — the authenticator, the
unused recovery codes, a texted step and any waiting challenge — and mails the person. Call it from an audited path:
[`Osysharp.UserManagement`](https://osysharp.com/reference/security/people-kit/)'s `ResetSecondFactor(person, reason)` records who did it and why, and signs the
person out everywhere. An app that requires the factor takes them through setting it up again at their next sign-in.

**A sign-in page of your own** hosts `SignInForm` — it carries every extra step — or calls the flow itself: `SignIn`,
`SignUp`, `SignInWithProvider`, `VerifyEmail`, `VerifyEmailCode` and `SignInWithPhone` answer a `SignInStep` (`Ticket`;
or `Challenge` with `ChallengeBy` — `"app"` or `"sms"`; or `MustEnrol` with its `Enrolment`; or `MustVerify`, the address
to prove; or `MustConfirmPassword`, an account's [first proof](#first-proof); or `Proven`, a link that needs nothing
more; or `CodeSent`; or — for a provider — `NoAccount`), never a bare ticket.

**The console signs in with the code too.** `osyrin login` (and every CLI sign-in to the app) runs the app's own
`SignIn` and then asks for the code — or takes `--code` — because the kit wires `SecondFactorSignIn`; an account that
must set a factor up is told to do so in a browser first ([[security-auth-bootstrap#second-factor]]).

### Signing in with a provider                {#providers}
Declare the providers as any app does, and the kit's sign-in form offers each one:

```osy syntax
use Osysharp.Oauth;                                       // in the manifest
app.Secrets = [ new Secret("GoogleClientSecret") ];
app.OAuthClients = [
  new OAuthClient("Google") {
    Provider = OAuthProvider.Google, Capabilities = [OAuthCapability.Login],
    ClientId = "…apps.googleusercontent.com", ClientSecret = Secret.GoogleClientSecret,
    Scopes = ["openid", "email", "profile"],
  },
];
```

A provider is offered only once it can sign somebody in on this installation: its `ClientId` is set and its
`ClientSecret` has a value (`osy secret set GoogleClientSecret` locally). One declared ahead of that — no client id yet,
or a secret with no value — draws no button, and the platform's `/api/oauth/authorize` refuses it, naming what is
missing. `OfferedSignInProviders()` is what every page reads, so a test states what is offered with
`OfferedSignInProviders.Stub(() => new List<string> { "Google" })` rather than depending on whose secrets it runs with.

"Continue with Google" goes to the provider and back to the platform, which verifies who the provider says this is and
hands it — sealed, so a browser cannot forge it — to the kit's `/oauth/sign-in` page. The platform signs nobody in by
itself: the kit wires the `OAuthSignIn` slot, and for an app that does, the callback resolves no account and issues no
ticket ([[security-auth-bootstrap#oauth-sign-in]]). `SignInWithProvider` then decides, in this order:

1. An identity already **linked** to an account signs in to that account.
2. Otherwise an account whose address the provider verified **and the account itself has proven** is linked, told by
   mail, and signs in. An account whose address nobody has proven is **not** handed over — it is mailed a link to prove
   it, and the person is told to sign in with the password or come back once proven.
3. Otherwise, when sign-up is open, an account is made for the address — proven already, with no password, with the
   same roles a password sign-up gets; when it is closed, the answer is `NoAccount`.

Whichever account it is, the rest is the same as after a password: a code when two-step sign-in is on, or setting it up
first when the app requires it.

Each button shows its provider's mark — GitHub, Google and Microsoft built in, any other the app's choice
(`AccountsSetup.Marks`, [ProviderIcon — the mark on every "Continue with …" and "Link …" button](https://osysharp.com/reference/security/provider-icon/)).

**Linking and unlinking** are on the account page — "Link Google" goes to the provider and back, and the page then asks
for the current password (and a code, with two-step sign-in on) before `LinkProvider` attaches it; `UnlinkProvider`
takes the password too. A borrowed session therefore cannot add a way in of its own, and an account can never unlink
its last way in (unlinking needs its password). An account that signed up with a provider has no password; it gets one
from "Forgot your password?", which mails a link to its proven address.

**An app with a door of its own** — an application form for strangers — takes the provider page over and hosts
`ProviderSignIn(ownDoor: true) { YourForm(); }`: on `NoAccount` (or `OwnDoor`, below) the slot is shown, and the form
reads the same sealed token from the address and calls the app's own `[AuthMethod]` (the `OAuthSignup` slot), which ends
in `ContinueSignIn`.

### An app's own door: making the account, proving a phone, coming back   {#own-door}
An app that lets strangers in only through a door of its own — an application to a waitlist, say — keeps `SignupOpen`
false and builds the door from four pieces, each the kit's own rule rather than a copy of it:

| Piece | What it does |
|---|---|
| `SignUpAtDoor(email, password, name)` | `SignUp` without asking `SignupOpen`: a new address gets an account and its proof; a taken one is answered exactly alike and its mail says so. Called from the app's own `[AuthMethod]`, wired in the `Signup` slot so it is capped like every sign-up |
| `TextPhoneCode(account, phone)` | texts a code that proves `phone` for an account the door has already established — a provider's sealed sign-in, an application it is finishing — where `ProvePhone` would ask for a password the account may not have. Answers the number as kept (E.164) |
| `ProvePhoneCode(account, phone, code)` | records the number as the account's, proven by that code; a wrong one is a wrong try, kept |
| `AccountsSetup.OwnDoor` | an `OwnDoorPolicy` whose `Unfinished(account)` says the door has not finished with this account. A provider sign-in for it answers `SignInStep.OwnDoor` and no ticket, so the provider page shows the door again instead of signing in |

```osy title="a door that finishes only once a phone is proven" syntax
public class ApplicationsInProgress : OwnDoorPolicy {
  public override bool Unfinished(User account) => Application.Any(a => a.Account == account && a.Waiting);
}
app.Accounts = new AccountsSetup { …, SignupOpen = false, ProvePhones = true, OwnDoor = new ApplicationsInProgress() };
```

**One phone, several accounts.** A number is not one account's: a person with more than one account proves the same
number on each, every time with a code texted for THAT account — a code texted to the number for one account is a
wrong code in another's proof. So `TextPhoneCode` texts a number other accounts hold like any other, and an app that
needs to count people must not count them by phone. Where a number would IDENTIFY an account — signing in by text — a
shared one identifies nobody (see `SmsSignIn` above).

### What does the app hear about each sign-in? {#events}
`AccountsSetup.Events` hears every completed sign-in — a password, a code, a provider, a sign-up, a reset link — once,
as the sign-in flow, so it may write what only that flow may:

```osy syntax
public class SignInHistory : AccountEvents {
  public override void SignedIn(User account, DateTime at) { new SignInDay { Account = account, At = at }; }
}
app.Accounts = new AccountsSetup { …, Events = new SignInHistory() };
```

An app's own door (an invitation, an application) ends in `ContinueSignIn(account)` — the same step the kit takes after
a password — so the second factor and `Events` hold on it too.

### The app's mark on the screens              {#logo}
`LogoUrl` puts the app's logo above its name on every sign-in screen, with no page taken over — an address the browser
fetches: a file the app keeps (`File.Url("public/brand/logo.svg")`) or an https address. An app page that frames a form
itself passes `AccountsFrame(heading, logo: …)`. (A `data:` address is not drawn — no page draws one.)

### Where are the pages?                      {#pages}
At the kit's routes, served by the kit — the mail links to these addresses, and `/login` is where a signed-out visitor
is sent. Each is authorized as it declares: the six a signed-out visitor needs (the five above and `/oauth/sign-in`) are `[AllowAnonymous]`, `/account`
requires a signed-in person. Every colour, face and size is a theme token, so the app's theme restyles them.

**To use a page of your own, take the kit's over** — declare a component of the same NAME. With a `[Route]` it is served
there instead (at the kit's route, or another); without one, the address is not served. The screens (`SignInScreen`, …)
and the bare forms (`SignInForm`, `SignUpForm`, …) are what your page renders:

```osy syntax
[Route("/login")] [AllowAnonymous] [Render(CSR)]          // takes the kit's SignInPage over
component SignInPage() { render { MyFrame { SignInForm(); } } }

[Route("/me")] [Render(CSR)]                              // moves the account page to /me
component AccountPage() { render { AccountScreen(); } }
```

An app page on one of these routes under ANOTHER name is refused, naming both — see [routes and pages](https://osysharp.com/reference/ui/routing/).

### What happens to the mail and texts it sends?   {#mail}
Reset, verification and notice mail go through [The Mail kit — send a message, keep a record, pick a sender](https://osysharp.com/reference/ui/mail-kit/)'s outbox, and texts through [The Sms kit — send a text message, keep a record, pick a carrier](https://osysharp.com/reference/ui/sms-kit/)'s. With no
sender (`app.Mail`, `app.Sms`), they are HELD — kept, with the reason — and the app still signs people up. Override
`AccountMails` and `AccountTexts` to write your own words.

### Erasing an account                         {#erasure}
`EraseAccount(account)` is what a person's right to erasure asks of the sign-in kit, done by the kit that knows every
table it keeps about them. The row stays — work it did still names it — and everything that says who it was goes: the
address (left as `ErasedAddress(account)`, on a domain that cannot receive mail), the name, the password, two-step
sign-in, a phone, every linked provider, every role, every session, every link and code ever sent to it, and the
outbox's copies of that mail (Osysharp.Mail's `EraseMailTo`). The account is left inactive.

It decides WHAT erasing means, never WHETHER somebody may be erased: call it from the run that erases — a workflow
body, as the system (an app's own erasure run, or a kit built on this one, as Osysharp.Shop's is). Called from a
request it writes as its caller and the kit's own rules refuse it.

### What this version does not do yet          {#not-yet}
Passkeys as a second factor (on the same challenge and the same `SecondFactor*` slots). Invitations, organisations and
the user manager are kits of their own, built on this one: [The Invitations kit — invite an address with a role, answered only by that address](https://osysharp.com/reference/security/invitations-kit/), [The Organizations kit — companies, teams, and roles held within one](https://osysharp.com/reference/security/organizations-kit/),
[The UserManagement kit — the user manager: who is in the app, what each may do, and who changed it](https://osysharp.com/reference/security/people-kit/).

### Step-up: prove it is you, now   {#step-up}
`StartStepUp(CodePurpose.PrivacyRequest)` mails a six-digit code to the signed-in account's own address;
`ProveStepUp(check, code)` takes it back; `SpendStepUp(check, account, purpose, within)` lets the action's own run (as
the system) spend a fresh proof once. Only the account's own person makes or proves one, and the code is the kit's
one-time code (fingerprinted, fifteen minutes, five tries, counted against the account). A session somebody left open
cannot take the action on its own. `Osysharp.Privacy.UserAccounts` uses it for a person's own privacy request.

## Examples       {#examples}

### A desk whose people are employees          {#example-desk}
```osy title="roles, the setting, and an app's own kind of account — the kit's pages are served for it" test app=security-accounts-kit
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, Agent, Lead }

app.Accounts = new AccountsSetup {
  AppName = "Acme Support", SignupRole = AppRole.Agent, FirstAccountRole = AppRole.Lead, Kinds = new DeskKinds(),
  SecondFactor = new LeadsUseCodes(),        // leads must use two-step sign-in
};

public class LeadsUseCodes : SecondFactorPolicy {
  public override bool Required(User account) { return RoleGrant.Any(g => g.Member == account && g.Role == AppRole.Lead); }
}

public class DeskKinds : AccountKinds {
  public override User New(string email, string name) { return new Employee { Email = email, Team = "Support" }; }
}

entity Employee : User {
  [MaxLength(60)] string Team = "";
  security {
    allow read when IsAuthenticated || IsAuthenticator;
    allow create when IsAuthenticator;
    allow update when IsAuthenticated || IsAuthenticator where Id == user.Id;
    allow update when IsAuthenticator;
  }
}

policy IsLead => RoleGrant.Any(g => g.Member == user && g.Role == AppRole.Lead);

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

## See also       {#see-also}
- [The Identity kit — who signs in, and which of the app's roles they hold](https://osysharp.com/reference/security/identity-kit/) — `Principal` and `RoleGrant`, which this kit builds on
- [The Mail kit — send a message, keep a record, pick a sender](https://osysharp.com/reference/ui/mail-kit/) — the outbox its mail goes through
- [`role` — a package's name for the app's own roles](https://osysharp.com/reference/security/role-type/) — how a package names the app's roles
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — the sign-in wiring the kit states for the app
- [entity Sub : Base](https://osysharp.com/reference/entity/inheritance/) — deriving `Employee : User`, and the package floor it stands on
- [routes and pages](https://osysharp.com/reference/ui/routing/) — how a package's pages are served, and taken over
- [Totp.NewSecret, Totp.Uri, Totp.Verify, Totp.Code](https://osysharp.com/reference/function/totp/) — the authenticator codes two-step sign-in is built on
- [Passkeys — sign in with the device, no password and no code](https://osysharp.com/reference/security/passkeys/) — passkeys: sign in with the device, several per account, password sign-in off
- [The Sms kit — send a text message, keep a record, pick a carrier](https://osysharp.com/reference/ui/sms-kit/) — the outbox its texted codes go through
- [OAuth account completion — Security.VerifyPendingOAuthEmail / LinkOAuthFromPending](https://osysharp.com/reference/security/oauth-completion/) — the sealed provider identity the provider sign-in reads
- [ProviderIcon — the mark on every "Continue with …" and "Link …" button](https://osysharp.com/reference/security/provider-icon/) — the mark on each provider's buttons, and giving another provider one
- [The Invitations kit — invite an address with a role, answered only by that address](https://osysharp.com/reference/security/invitations-kit/) — bringing a person in by invitation, with sign-up closed
- [The UserManagement kit — the user manager: who is in the app, what each may do, and who changed it](https://osysharp.com/reference/security/people-kit/) — the user manager: roles, deactivation, a lost second factor, sign-out everywhere
