Summary#
app.Auth declares how a user of your app is authenticated generically — by the platform, with no code from
you. For a password, you bind two properties of your [[security-entity-security|[Principal]]] entity: the one that
carries the login, and the one that stores the hash.
[Principal]
entity User {
[Required, MaxLength(255)] string Email;
[MaxLength(200)] string PasswordHash;
security {
allow read when IsAuthenticated;
allow read where Id == user.Id;
// A read grant is a ROW grant, so without this the hash rides out with the row — to an admin listing users,
// to an export, to the user's own page.
//
// CONDITIONED, and the condition is the sign-in itself. A masked column is DROPPED from the read, so an
// unconditional deny would hide the hash from the verification too and refuse a correct password exactly as it
// refuses a wrong one. Sign-in happens while you are still ANONYMOUS — and an anonymous caller already gets no
// rows here, so this one line says "verify me, then never show me" with nothing else to declare.
deny read PasswordHash when IsAuthenticated;
}
}
app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };That is enough for the platform to create a user of your app and authenticate one: it hashes the password,
verifies it, and issues the ticket, reading and writing the fields you named. The field names come from this
declaration — nothing about Email or PasswordHash is hardcoded, and your properties may be called anything.
Signature#
app.Auth = new PasswordAuth { LoginField = <PrincipalProp>, PasswordField = <PrincipalProp> };
app.Auth = new OAuthAuth { Client = <OAuthClient> };
app.Auth = [ new PasswordAuth { … }, new OAuthAuth { … } ]; // several methods, offered togetherLoginField and PasswordField are both required, and each must name a property of the [Principal] entity
(a bare name, not a string) — an unknown name is a compile error, so the binding cannot rot when you rename a
property. app.Auth is a singleton: declare it once. Its value may be a single method or a list, when an app
offers more than one way to sign in.
Description#
app.Auth and [AuthMethod] are two different doors#
This is the distinction to get right, and the one that reads as confusing until you see it:
app.Auth (PasswordAuth) | **[[security-auth-method | [AuthMethod]]]** + app.AuthBootstrap | |
|---|---|---|---|
| Who runs the sign-in | the platform, generically | your function, written in Osy# | |
| You write | two field bindings | a Login function (and usually a Signup) | |
| Who calls it | the tooling — provisioning a user, the console/API login | your app's own login page | |
| Hashing / verifying / the ticket | the platform does it | you do it, with Security.* | |
| Custom rules (invite-gated signup, a first-admin grant, a lockout) | not possible — it is generic by design | anything you can write |
They are not alternatives to choose between. A real app usually declares both, and for good reason: app.Auth
gives the platform a way to create and authenticate a user of your app before you have built any of it (and keeps
osy-side tooling working forever after), while your [AuthMethod] Login is the sign-in your users actually see,
where you control the flow. The platform's own Admin app — the most complete app we have — declares both.
Concretely: your [AuthMethod] Login does not consult app.Auth. It reads the user, calls
Security.VerifyPassword, and returns Security.IssueJwt(...) itself. Nothing is shared between the two paths except
the rows in your [Principal] table — which is exactly why they interoperate: a user the platform created with
app.Auth can sign in through your login page, and a user your Signup created can be authenticated by the tooling.
Both hash passwords the same way, so the hash column is meaningful to both.
Why the platform needs the binding at all#
The platform does not know what your user entity looks like. It cannot assume a property called Email, or that the
hash lives in PasswordHash — your app might have Username and Secret, or a Norwegian app might call it
Epost. app.Auth is the two-line answer to "which column do I compare, and which one do I hash into", and it is
compile-checked against the [Principal], so it cannot drift out of step with the entity it names.
Declare no app.Auth, and the generic path simply reports that the app declares no password method — the tooling
cannot provision or authenticate a user. Your own [AuthMethod] Login still works, because it never needed it.
Offering OAuth instead of, or beside, a password#
new OAuthAuth { Client = <name> } authenticates against an OAuth client you declared in app.OAuthClients, instead
of (or alongside) a password. Give a list to app.Auth when an app offers both, and the caller picks:
app.Auth = [
new PasswordAuth { LoginField = Email, PasswordField = PasswordHash },
new OAuthAuth { Client = Google },
];Examples#
The full shape, as a real app declares it — the generic binding and the app's own login, side by side:
[Role] enum AppRole { Authenticator, Member }
entity RoleGrant {
[Required] User User;
[Required] AppRole Role = AppRole.Member;
security { allow create when IsAuthenticator; }
}
policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);
// The app's OWN login — it verifies and issues the ticket itself; app.Auth is not involved.
[AuthMethod]
string Login(string email, string password) {
var u = User.Where(x => x.Email == email).FirstOrDefault();
if (u == null) { return ""; }
if (Security.VerifyPassword(password, u.PasswordHash)) { return Security.IssueJwt(u.Id, u.Email); }
return "";
}
app.AuthBootstrap = new AuthBootstrap { Role = AppRole.Authenticator, Login = Login };(The User entity and the app.Auth line are the ones at the top of this page — the two declarations coexist in the
same app, and neither knows about the other.)
See also#
- [AuthMethod] — a function an unauthenticated visitor may call — your own login function, and why it needs no
app.Auth - auth bootstrap (login, before anyone is signed in) — the wiring that lets a signed-out visitor reach that function
- Security.* — hashing, verifying, tickets, random ids — the hashing, verifying and ticket-issuing your own login does by hand
- security { } — the
[Principal]entity whose fields this binds