# Security.* — hashing, verifying, tickets, random ids

> The calls an authentication flow needs: `HashPassword` (salted, one-way), `VerifyPassword` (constant-work comparison against a stored hash, and a one-argument form for the no-such-account path), `IssueJwt` (the session ticket a login returns), and `RandomId`/`RandomHex` for unguessable tokens. They are the building blocks of an `[AuthMethod]` login/signup — you never store a plaintext password, and you never compare hashes yourself.

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

## Summary        {#summary}
`Security.*` is what an authentication function is built from — hash a password on the way in, verify it on the way
back, and hand out a ticket:

```osy title="the three that make a login" test app=stdlib-security
[AuthMethod]
string Login(string email, string password) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  // No such account. Verify against nothing anyway — it costs the same as a real check, so the clock does not
  // tell a stranger which addresses are registered. See "the no-account path" below.
  if (u == null) { Security.VerifyPassword(password); return ""; }
  if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
  return "";
}

[AuthMethod]
string Signup(string email, string password) {
  var u = new User { Email = email, PasswordHash = Security.HashPassword(password) };
  return Security.IssueJwt(u.Id, u.Email);
}
```

## Signature      {#signature}
```osy syntax
string Security.HashPassword(string plain)                   // salted one-way hash — store THIS, never the password
bool Security.VerifyPassword(string plain, string hash)     // does the plaintext match the stored hash?
bool Security.VerifyPassword(string plain)                        // no account to check — spend the time, answer false
string Security.IssueJwt(Guid userId, string email)         // the session ticket a login/signup returns
void Security.EndOtherSessions(Guid userId)                 // end the account's other tickets, when this commits
void Security.SignOutEverywhere(Guid userId)                // an administrator ends somebody's tickets, when this commits
int Security.SignedInElsewhere(Guid userId)                 // how many live tickets the account holds besides this one

string Security.RandomId()                                  // an unguessable id, default length
string Security.RandomId(int length)                        // …of a given length
string Security.RandomHex(int length)                       // random hex characters

string Security.NewApiKey()                                 // a new API key (`pk_…`) — show it once, keep its hash
string Security.HashApiKey(string key)                      // what is stored for that key on its principal, in this app
```

## Description    {#description}

### How do I hash a password, and check one?   {#passwords}
`HashPassword` produces a **salted, one-way hash**. The salt is generated for you and travels inside the returned
value, so two users with the same password get different hashes — and the same password hashed twice is never the
same value. The algorithm is Argon2id (memory-hard, at OWASP's parameters), and its parameters are stored in the hash
too, so a hash written by an older setting — or a bcrypt one — still verifies. That has a consequence worth stating,
because it surprises people: **you cannot compare hashes**.

```osy title="✗ two hashes of one password never match — verify instead" syntax
if (u.PasswordHash == Security.HashPassword(password)) { … }   // ❌ never true. Not "insecure" — WRONG.
if (Security.VerifyPassword(password, u.PasswordHash)) { … }   // ✅ the only way to check a password
```

A hash is an ordinary `string`, and the column you store it in is an ordinary bounded one:

```osy title="a hash is an ordinary bounded string column" syntax
[MaxLength(200)] string PasswordHash;      // the column a hash goes in
```

⚠ **Nothing about the TYPE stops you comparing hashes** — both sides of the ❌ line above are strings, so it compiles
and is simply always false. What catches it is `osy lint`, which reports it as `security-password-compared-directly`
at MUST tier. The guarantee that the hash never leaves is carried entirely by the field mask below, not by the type.

`VerifyPassword` takes the **plaintext first, the stored hash second** — the order matters, and swapping them fails
every login. It re-derives the hash with the salt it finds in the stored value and compares them safely.

#### When no account matches, still spend the hash time — `VerifyPassword(plain)`   {#no-account}

A login that returns the moment no row matches is giving a **correct answer with the wrong timing.** Hashing is
deliberately slow — that is what a password KDF is for — so an unknown address answers in microseconds where a known
one takes the full work factor. The response body is identical and the clock is not, so anyone can feed a list of
addresses to a page that is *meant* to be public and learn which of them hold accounts. That is a disclosure on its
own (who banks here, who uses this clinic) and it is the first half of every credential-stuffing run.

The one-argument form is the fix, and it is a security primitive rather than a convenience — it verifies against
nothing, pays the same KDF, and answers `false`:

```osy syntax
var u = User.Where(x => x.Email == email).FirstOrDefault();
if (u == null) { return ""; }                                  // ❌ answers in microseconds — an enumeration oracle
if (u == null) { Security.VerifyPassword(password); return ""; }   // ✅ costs what a real check costs
```

`osy lint` reports the ❌ shape as `security-login-enumerates-users`, at MUST tier.

⚑ `VerifyPassword(password, "")` does the same thing — an empty stored hash still costs a full verify, deliberately,
so that a row with no credential (an OAuth-only account, a truncated column) cannot answer faster than a wrong
password either. Prefer the one-argument form: it says "there is nothing to check" rather than leaving the reader to
work out what an empty second argument means.

Store only the hash. The plaintext password should exist nowhere in your app: not in a column, not in a log, not on a
second entity "for the reset flow". `HashPassword` at the point of signup is the whole story, and the
[field mask](https://osysharp.com/reference/security/entity-security/) (`deny read PasswordHash when !IsAuthenticator`) is how you make sure the hash
itself is never read by anything but the login.

### How do I mint a session ticket? — `IssueJwt`   {#issuejwt}
`Security.IssueJwt(userId, email)` mints the **session ticket** — a signed token, scoped to your app and to that user.
Return it from your `[AuthMethod]` `Login`/`Signup`, and the login page hands it to `Session.SignIn(ticket)`, which
stores it as the session bearer: the next request arrives authenticated as that user, carrying whatever roles they
have been granted.

Two rules follow from what the ticket *is*:

- **Issue it only after you have verified the credential.** `IssueJwt` does not check anything — it signs whatever
  user you name. It is the *conclusion* of a login, never a step in one.
- **Return `""` for a failed sign-in**, not a ticket and not an exception. `Session.SignIn("")` stores nothing and the
  visitor stays anonymous. Returning the same empty answer whether the email is unknown or the password is wrong is
  also what stops `Login` from telling a stranger which addresses have accounts.

A ticket carries the grants the user has **at the moment the next request is served** — it is a claim of identity, not
a frozen snapshot of permissions. Grant a role during signup (see [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/)) and it is in force
immediately.

### How do I sign the account out everywhere else? — `EndOtherSessions`   {#end-other-sessions}
`Security.EndOtherSessions(account.Id)` ends **every ticket the account already holds in this app, except the one the
calling request carries** — on every browser and every server, on its next request. Call it where the account's way in
changes: a password change, a reset, or (as the [[security-accounts-kit#sessions|UserAccounts kit]] does) the first proof of
an address that claims an account from a stranger who signed up with it first. The account's sessions are then valid
from that moment.

```osy title="a password change signs the account's other browsers out" test app=stdlib-security
[AuthMethod]
string ChangePassword(string email, string currentPassword, string newPassword) {
  var u = User.Where(x => x.Email == email).FirstOrDefault();
  if (u == null || !Security.VerifyPassword(currentPassword, u.PasswordHash)) {
    throw new ValidationException("That is not your current password.");
  }
  Security.EndOtherSessions(u.Id);
  u.PasswordHash = Security.HashPassword(newPassword);
  return "";
}
```

- **The sessions are chosen at the call and ended at the commit.** A ticket issued after the call — the one a reset
  hands back, a sign-in a second later — is not among them, so a reset can end everything and then sign the person in.
  And a change that is rolled back (a refusal later in the same function) signs nobody out.
- **The person making the change stays signed in**; nobody else holding the account does. A reset made from a
  signed-out browser has no session of its own, so it ends them all.
- **The refusal says why.** The ended ticket's next request answers 401 with *"You were signed out because this
  account's password was changed or reset in another session. Sign in again."*, and a live socket it holds closes.
- It ends sessions of **this** app only, and only the account you name.

### How does an administrator sign somebody out? — `SignOutEverywhere`   {#sign-out-everywhere}
`Security.SignOutEverywhere(person.Id)` is the same ending as `EndOtherSessions`, made by somebody who looks after the
account rather than by its owner — deactivating it, resetting its second factor, or answering "I lost my laptop". Every
ticket the person holds ends on its next request, and **the refusal says who did it**: *"You were signed out because
somebody who looks after this app's people changed this account's access. Sign in again."* — not that a password
changed, which the person did not do. `Security.SignedInElsewhere(person.Id)` answers how many live tickets the account
holds besides the calling request's — what the call would end — so a page can show it before anybody presses anything.
[`Osysharp.UserManagement`](https://osysharp.com/reference/security/people-kit/) is built on both.

```osy title="a manager signs a lost laptop out, and can see how many sessions it ends" test app=stdlib-security
int OtherSessions(User u) { return Security.SignedInElsewhere(u.Id); }

void SignOutLostLaptop(User u) {
  Security.SignOutEverywhere(u.Id);
}
```

Like `EndOtherSessions`, the sessions end when the call's work commits, and a change rolled back signs nobody out.

### How do I make an unguessable token? — `RandomId` / `RandomHex`   {#random}
Cryptographically random strings, for the things that must be **unguessable**: a password-reset token, an invite code,
an API key, a one-time link. `RandomId()` takes a default length, `RandomId(length)` a chosen one, and
`RandomHex(length)` gives you hex characters.

```osy title="an api key nobody can guess" test app=stdlib-security
string IssueApiKey(User owner) {
  var key = Security.RandomId(32);
  new ApiKey { Owner = owner, Token = key };
  // Handed back HERE, once, to the caller who asked for it. The row itself never gives it up again — see the
  // `deny read Token` on `ApiKey` below. That is how the platform's own keys work too: `osy user apikey generate`
  // prints the `pk_…` once and stores only what it needs to check it.
  return key;
}
```

Do not reach for these for a database key: an entity's `Id` is already a unique identifier. Reach for them when the
value's job is to be **secret**.

⚠ **A secret whose recipient is NOT the caller — a reset token, an invite code, a one-time link — is only half done
when you have minted it.** Its whole purpose is to travel out of band and come back, so the app has to be able to
send it: a [client class](https://osysharp.com/reference/http/client/) over `Http.*`, a secret for the provider, and a call. Mint it and stop, and the
step that later asks for it can be satisfied by nobody, however green the tests are — `osy lint` reports that as
`security-reset-token-never-delivered` (MUST). [[security-auth-bootstrap#examples]] shows a password reset with its
delivery wired, and [The UserAccounts kit — sign-in done properly, once](https://osysharp.com/reference/security/accounts-kit/) is the same flow done once for every app — `demo/auth-demo` uses it end to end with tests.

⛔ **And never hand such a token back to the caller who asked for it.** This example returned
`Security.RandomId(32)` from an `[AuthMethod] string StartReset(string email)` until 2026-09-04 — so anyone who
knew your address could ask for your reset token and be given it, which is precisely the account takeover the
out-of-band channel exists to prevent. The value goes to the mailbox; the caller learns nothing either way.

### How do I make an API key? — `NewApiKey` / `HashApiKey`   {#api-keys}
An API key lets a program call the app's REST API as one person (`X-API-Key`). `Security.NewApiKey()` makes a key the
way the platform makes every key; `Security.HashApiKey(key)` is what is stored for it in this app, on the person's own
row (`ApiKeyHash`, or `ApiKeyHash2` for a second key). Neither writes anything: who may put a hash on whose account is
your app's `security {}`. You rarely need either directly — [[security-people-kit#api-keys|the UserManagement kit]] issues and
revokes keys from a settings page, through these two:

```osy syntax
var key = Security.NewApiKey();          // show it once
var hash = Security.HashApiKey(key);     // keep this — a request presenting `key` then acts as its holder
```

Both run only on the server.

### Has this secret been given a value? — `IsSecretSet`   {#issecretset}
A declared secret has no value until someone sets one (`osy secret set <NAME>`), and reading an unset secret fails
at the point of use — which is usually deep inside the call that needed it. `Security.IsSecretSet("NAME")` answers
whether it has one, so a feature that depends on a secret can say so plainly instead:

```osy title="degrade with a message, rather than failing where the secret is read" syntax
string Summarise(string body) {
  if (!Security.IsSecretSet("OPENAI")) { return "Summaries are off — no OPENAI key is configured."; }
  return Llm.Complete(body);
}
```

It answers whether a value EXISTS, never what it is — there is no verb that reads a secret out into your code.

### Who is acting, when I have no principal type? — `CurrentUserId`   {#current-user-id}
`Security.CurrentUserId` is the id of the principal the code is running for — the signed-in person, or the actor a
body runs as — and `null` when nobody is signed in. An app reads the whole row with `Session.CurrentUser`; a
**package** cannot, because the `[Principal]` entity is the app's and the package was written before the app
existed. The id is what a package can hold: a `Guid?` it can store, compare and hand back.

```osy title="a package records who an action ran for, without knowing the app's principal type" syntax
entity AuditLine {
  Guid? Actor;
  [MaxLength(200)] string What = "";
}

void Note(string what) { new AuditLine { Actor = Security.CurrentUserId, What = what }; }
```

It runs only on the server: who is acting is the server's to say.

## Examples       {#examples}
The declarations the examples above are written against — the credential entity, the roles, and the wiring that makes
`Login`/`Signup` reachable while signed out:

```osy title="the app these functions live in" test app=stdlib-security
[Role] enum AppRole { Authenticator, Member }

[Principal]
entity User {
  // [Unique] or two rows share a login, and which account a password opens is then undefined — a race no
  // check-then-insert in application code can close, because the race is in the database.
  [Required, Unique, MaxLength(255)] string Email;
  [MaxLength(200)] string PasswordHash;          // the HASH — the password itself is stored nowhere
  security {
    allow read   when IsAuthenticator;
    allow create, update when IsAuthenticator;
    allow read where Id == user.Id;
    deny read PasswordHash when !IsAuthenticator; // and only the auth flow ever reads even the hash
  }
}

[RoleGrant] entity RoleGrant {
  [Required] User User;
  [Required] AppRole Role = AppRole.Member;
}

entity ApiKey {
  [Required] User Owner;
  [MaxLength(64)] string Token = "";
  security {
    allow read, create where Owner == user;   // the rows are yours; the SECRET on them is nobody's
    // ⛔ A ROW GRANT IS NOT A FIELD GRANT, and "only the owner can read it" is not a reason to skip this. A
    //    readable stored secret is readable forever, by every later page, export and audit that touches the row.
    //    `IssueApiKey` hands the value to its caller once; after that there is nothing left to read.
    deny read Token;
  }
}

policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);

app.AuthBootstrap = new AuthBootstrap {
  Role          = AppRole.Authenticator,
  Login          = Login,
  Signup         = Signup,
  PasswordChange = ChangePassword,
};
```

## See also       {#see-also}
- [[AuthMethod] — a function an unauthenticated visitor may call](https://osysharp.com/reference/security/auth-method/) — the `[AuthMethod]` marker these functions carry
- [auth bootstrap (login, before anyone is signed in)](https://osysharp.com/reference/security/auth-bootstrap/) — the identity they run as, and what it is allowed to touch
- [app.Auth — how the platform authenticates a user of your app](https://osysharp.com/reference/security/password-auth/) — `app.Auth`, where the platform does the hashing and ticket-issuing for you
- [role grants (and the first admin)](https://osysharp.com/reference/security/role-grants/) — granting a role at signup without opening a path to self-elevation
