# The Identity kit — who signs in, and which of the app's roles they hold

> `Osysharp.Identity` is the contract every people-aware kit builds on: `Principal`, the one `[Principal]` of the app (an address, a display name, whether it may sign in, whether its address was proven), and `RoleGrant`, which holds the app's OWN `[Role]` members. It has no password and no pages — `Osysharp.UserAccounts` is the default way in; an app that signs people in some other way derives its own type from `Principal`.

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

## Summary        {#summary}
`use Osysharp.Identity@0;` gives an app its `[Principal]` — `Principal` — and its grant table — `RoleGrant` — so that
every kit that knows about people (accounts, invitations, organisations, a user manager) references the same two
types. The app keeps its own roles:

```osy syntax
[Role] enum AppRole { Authenticator, Agent, Lead }
policy IsLead => RoleGrant.Any(g => g.Member == user && g.Role == AppRole.Lead);
```

## Signature      {#signature}
```osy syntax
[Principal] public entity Principal {
  string Email;                 // [Required, Unique], stored trimmed and lower-case by the flows that write it
  string DisplayName = "";
  bool IsActive = true;         // an inactive account cannot sign in; it is kept, not deleted
  string? ExternalId;           // the identity provider's id, for an account it provides
  IdentitySource Source;        // Local | Idp
  DateTime? EmailVerifiedAt;    // when a message sent to Email was received and acted on
  DateTime? LastLoginAt;
  string? ApiKeyHash;           // an API key's hash — how a program (a service, an agent) signs in; minted by the platform
  string? ApiKeyHash2;          // a second key, so one can be replaced with no moment when neither works
}
[RoleGrant] public entity RoleGrant { Principal Member; role Role; }       // `role` = the app's [Role] enum
policy IsAuthenticator => RoleGrant.Any(g => g.Member == user && g.Role == role.Authenticator);
string NormalisedEmail(string email);   bool LooksLikeEmail(string email);   Principal? CurrentPrincipal();
string PersonName(Principal p);         // DisplayName, else Email, else "" (a system account) — one expression, so a render may call it
bool IsUnreachableAddress(string email); // under the reserved `.invalid` domain: a system account, mailed nothing
```

## Description    {#description}

### One principal, many kinds of account       {#kinds}
`Principal` is the app's one `[Principal]`; every kind of account derives from it and shares its table, so `[Unique]
Email` means **one address, one account** across all of them. In every security rule `user` is the base `Principal`,
and `user is Member` (or `Session.CurrentUser is Member`) tells the kinds apart. A type derived from `Principal` signs
in through a sign-in written against the base, and each kind states its own `security { }` grants.

### What every kind stands on                  {#floor}
The package's rules are the **floor** of every type derived from `Principal` ([entity Sub : Base](https://osysharp.com/reference/entity/inheritance/)): an account
edits its own `DisplayName`, but `Email`, `EmailVerifiedAt`, `Source`, `ExternalId`, `IsActive` and `LastLoginAt` are
written only by the sign-in flow (`IsAuthenticator`) — whatever the derived type's own block grants. So nobody marks
their own address proven, reactivates themselves, or changes the address they sign in with except through a verified
change. The two API-key hashes are credentials: read by the sign-in flow alone, written by nobody signed in (the
platform mints and revokes keys — `osy user apikey generate` — outside the app's rules).

**Everybody signed in sees everybody** — a team's directory. An app whose people must not see each other narrows it on
the type its accounts ARE — `partial entity User { security { deny read when !IsLead where Id != user.Id; } }` for an
Accounts app. A deny stated on `Principal` itself holds too — on every type derived from it ([entity Sub : Base](https://osysharp.com/reference/entity/inheritance/)).

### Roles are the app's                        {#roles}
`RoleGrant.Role` is typed `role` — the app's own `[Role]` enum, bound when the package is compiled into the app
([`role` — a package's name for the app's own roles](https://osysharp.com/reference/security/role-type/)) — so a grant is an ordinary row of the app's vocabulary and the platform resolves it like any
grant ([role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/)). The app's enum must have an `Authenticator` member: that is the role the sign-in flow
runs as, and `IsAuthenticator` names it.

- **Each person reads their own grants and nobody else's.** What a role unlocks is answered from the caller's own role
  names, never by reading the table. The sign-in flow reads them too — deciding what a sign-in asks for ("every admin
  uses two-step sign-in") needs to know who holds which role — and it already grants them.
- **Who may grant a role is the app's decision**: the table is `[Authority(Extensible)]`, so an app states it beside
  its roles — `partial entity RoleGrant { security { allow read, create, delete when IsAdmin; } }`.
- **Unscoped.** A role here holds everywhere in the app; a role within one organisation is a different table's row
  ([[security-organizations-kit#roles]]).
- **One row per person and role.** `RoleGrant` is `[Unique(Member, Role)]`, so two promotions of one person made at
  the same moment cannot leave two identical grants: the later commit is refused with *"They already hold that role."*
  A grant path that may race takes that as done — [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/)'s `GrantRole` records its change again and
  it applies as nothing left to do. Granting a role somebody holds is not an error to guard against at every call
  site; check first (`if (!RoleGrant.Any(…))`) and let the index settle the race the check cannot see.

### Which roles people are offered             {#offered}
`app.Roles` says which of the app's roles its pages hand out, in order, and how each reads to a person. Kits that offer
a role — an invitation form, a people manager — offer exactly these, and refuse any other: a role left out (an
integration's, the sign-in flow's `Authenticator`) is never offered.

```osy syntax title="the roles a page may hand out, in order, with how each reads"
app.Roles = new RolesSetup { Offered = [
  new RoleChoice { Role = AppRole.Agent, Label = "Agent", Description = "Works tickets." },
  new RoleChoice { Role = AppRole.Admin, Label = "Admin", Description = "Runs the desk." },
] };
```

`OfferedRoles()` answers them for a page, `IsOfferedRole(r)` asks of one, and `RoleLabel(r)` gives its name. Each
`RoleChoice` also says where the role may be held — `HeldIn = RoleScope.App`, `RoleScope.Organization`, or either (the
default) — and the kits offer and give it only there ([RoleChoice.HeldIn — where a role may be held: across the app, inside an organisation, or either](https://osysharp.com/reference/security/roles-held-in/)). Changing who
holds a role through a page, with a record of who did it, is [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/).

A role a page should NAME but never hand out — an operator's role, an integration's — is listed with `Grantable = false`:
`RoleLabel(r)` names it wherever somebody holds it, while `OfferedRoles()` leaves it out, `IsOfferedRole(r)` answers
false, and the kits refuse to grant or take it.

```osy syntax title="a role a page names but never hands out"
new RoleChoice { Role = AppRole.Operator, Label = "Operator", Description = "Runs the platform.", Grantable = false }
```

### When somebody is given a role, or loses one   {#role-events}
`RolesSetup.Events` is what the app does about it — put a new agent on the desk's queues, take a leaver off them. The
kits that change roles tell it: the user manager ([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/)) for a grant or a revoke, an accepted
invitation ([The Invitations kit — invite an address with a role, answered only by that address](https://osysharp.com/reference/security/invitations-kit/)) for the role it was sent with, and a sign-up ([The UserAccounts kit — sign-in done properly, once](https://osysharp.com/reference/security/accounts-kit/)) for
each role the app gives a new account (`AccountsSetup.SignupRole`, `FirstAccountRole`). It runs inside that change's own
write, as the system — so what it writes lands with the grant or not at all, and needs no grant of the sign-in flow's —
and it is told once per change that took effect.

```osy syntax
public class DeskRoles : RoleEvents {
  public override void Granted(Principal person, role r) { JoinTheDesk(person); }   // which queues is the app's call
  public override void Revoked(Principal person, role r) { LeaveWhatNoLongerFits(person); }
}
app.Roles = new RolesSetup { Offered = [ … ], Events = new DeskRoles() };
```

### Signing in                                 {#sign-in}
The kit has no password: [The UserAccounts kit — sign-in done properly, once](https://osysharp.com/reference/security/accounts-kit/) is the default way in, and adds `User : Principal` with the
credential. An app with its own way in (a PIN, a company's identity provider) derives from `Principal` and writes its
`[AuthMethod]`s — see the example.

## Examples       {#examples}

### A club that signs members in with a number and a PIN   {#example-pin}
```osy title="the app's own kind of account and sign-in, on the contract's directory and roles" test app=security-identity-kit
app Club {
  use Osysharp.Identity;
  model "model/**/*.osy";
}

using Osysharp.Identity;

[Role] enum ClubRole { Authenticator, Member, Steward }
policy IsSteward => RoleGrant.Any(g => g.Member == user && g.Role == ClubRole.Steward);

entity Member : Principal {
  [Required, Unique, MaxLength(20)] string Number;
  [MaxLength(200)] string PinHash;
  security {
    allow read when IsAuthenticated || IsAuthenticator;
    allow create when IsAuthenticator;
    allow update when IsAuthenticated || IsAuthenticator where Id == user.Id;
    allow update when IsAuthenticator;
    deny read PinHash when !IsAuthenticator;
  }
}

entity StewardNote {
  [Required, MaxLength(200)] string Body;
  security { allow read, create when IsSteward; }
}

[AuthMethod]
string SignInWithPin(string number, string pin) {
  var m = Member.Where(x => x.Number == number.Trim()).FirstOrDefault();
  if (m == null || !m.IsActive) { Security.VerifyPassword(pin); return ""; }   // a miss spends a hash too
  if (!Security.VerifyPassword(pin, m.PinHash)) { return ""; }
  m.LastLoginAt = DateTime.UtcNow;
  return Security.IssueJwt(m.Id, m.Email);
}

[AuthMethod]
string Join(string email, string number, string pin) {
  if (!LooksLikeEmail(email)) { throw new ValidationException("That does not look like an email address."); }
  var m = new Member { Email = NormalisedEmail(email), Number = number.Trim(), PinHash = Security.HashPassword(pin) };
  new RoleGrant { Member = m, Role = ClubRole.Member };
  return Security.IssueJwt(m.Id, m.Email);
}

bool AmMember() { return Session.CurrentUser is Member; }

app.AuthBootstrap = new AuthBootstrap { Role = ClubRole.Authenticator, Login = SignInWithPin, Signup = Join };
```

## See also       {#see-also}
- [The UserAccounts kit — sign-in done properly, once](https://osysharp.com/reference/security/accounts-kit/) — the default way in: `User : Principal`, sign-up, reset, verification, pages
- [`role` — a package's name for the app's own roles](https://osysharp.com/reference/security/role-type/) — how the kit names the app's roles
- [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, which grants and revokes the offered roles
- [RoleChoice.HeldIn — where a role may be held: across the app, inside an organisation, or either](https://osysharp.com/reference/security/roles-held-in/) — where an offered role may be held: across the app, inside an organisation, or either
- [entity Sub : Base](https://osysharp.com/reference/entity/inheritance/) — deriving from `Principal`, and the floor it stands on
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — wiring an `[AuthMethod]` sign-in
