Summary#
A function marked [AuthMethod] may be called by a visitor who is not signed in. Every other function in your
app refuses an unauthenticated caller before its first statement runs — which is exactly what you want, and is why
sign-in needs a marker of its own: Login cannot require a signed-in user, because producing one is its job.
[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 ""; // no match → no ticket → the caller stays anonymous
}The marker alone is not enough, and that is deliberate: the function must also be wired into
app.AuthBootstrap (as Login, Signup or PasswordReset). The two halves are checked
against each other, so you cannot get one without the other.
Signature#
[AuthMethod] <ReturnType> <Name>(<params>) { … } // callable while signed out; must be wired in app.AuthBootstrapA Login or Signup returns the session ticket — a string from Security.IssueJwt — or
"" for "no". A PasswordReset typically returns nothing of value; it starts a flow (mails a link, mints a token).
Description#
Do I need both [AuthMethod] and app.AuthBootstrap?#
[AuthMethod] and app.AuthBootstrap must agree, and the compiler enforces it in both directions:
| You wrote | What happens | Why |
|---|---|---|
[AuthMethod] on a function not wired in app.AuthBootstrap | compile error | It is an anonymously-reachable entry point that no sign-in flow uses. That is a hole, and a hole is never intentional. |
A function wired in app.AuthBootstrap without [AuthMethod] | compile error | The signed-out visitor would be refused before your login could run — a login page that can never log anyone in. |
| Both | it works | The only way to get an anonymous entry point is to say so twice. |
There is no [AllowAnonymous] on a function. That marker belongs to pages (page authorization (policies)); a function opens to the
world only through this one purpose-built door, so grep AuthMethod is a complete list of your app's anonymous entry
points. It should be a short list.
What an auth method may touch#
An [AuthMethod] does not run with special powers. It runs as the ephemeral principal described in
auth bootstrap (login, before anyone is signed in) — a caller bearing the one [Role] you named, and no user. What it may read and write
is decided by your ordinary security { } grants for that role, and nothing else:
[Role] enum AppRole { Authenticator, Member, Admin }
[Principal]
entity User {
[Required, MaxLength(255)] string Email;
[MaxLength(200)] string PasswordHash;
security {
allow read when IsAuthenticator; // login must find the user and check the hash
allow create when IsAuthenticator; // signup must create one
allow read where Id == user.Id; // and a signed-in user reads their own row
deny read PasswordHash when !IsAuthenticator; // nobody else EVER reads the hash — not even an admin
}
}
entity RoleGrant {
[Required] User User;
[Required] AppRole Role = AppRole.Member;
security { allow create when IsAuthenticator; } // the auth flow may mint a grant…
}
entity Invoice { // …and it may not touch anything else.
[Required] decimal Total;
security { allow read where User.Any(u => u.Id == user.Id); }
}
policy IsAuthenticator => RoleGrant.Any(g => g.User == user && g.Role == AppRole.Authenticator);Login above can read User — including PasswordHash, which the field mask hands to no one else. It cannot read
an Invoice, because nothing granted Authenticator a read on one. If you never grant the auth role a write, no
routed method can be turned into a writer, however the function is written. The leash is your own rules, which is
the whole point: the bootstrap path is not a hole you have to remember — it is subject to the same grants you can read
on the entity.
It cannot elevate a caller who is already signed in#
The ephemeral principal is minted only when the caller has no authenticated ticket. An already-signed-in user who
calls Login is not elevated — they run as themselves, with their own grants. So the bootstrap path can never be
used by an ordinary user to borrow the auth role's access.
What should a login RETURN, and what does a failure return?#
A Login/Signup ends by returning what Security.IssueJwt(userId, email) produced. That string
is the session ticket, scoped to your app and that user. The login page hands it to Session.SignIn(ticket), which
stores it as the session bearer, so every later request is authenticated as that user.
A failed sign-in returns "" — not an exception, not a partial ticket. Session.SignIn("") stores nothing and the
visitor stays anonymous. Returning the same empty answer for "no such user" and "wrong password" is also what keeps
Login from telling a stranger which email addresses have accounts.
Examples#
Signup, wired as the second auth method — it creates the credential and signs the new user straight in:
[AuthMethod]
string Signup(string email, string password) {
var u = new User { Email = email, PasswordHash = Security.HashPassword(password) };
var grant = new RoleGrant { User = u, Role = AppRole.Member };
return Security.IssueJwt(u.Id, u.Email);
}
[Page("/login")]
[AllowAnonymous]
[Render(CSR)]
component LoginPage() {
string email = "";
string password = "";
action SignIn() { Session.SignIn(Login(email, password)); }
render {
Input(value: email, placeholder: "Email");
Input(value: password, placeholder: "Password");
Button("Sign in", onPress: SignIn);
}
}
app.Auth = new PasswordAuth { LoginField = Email, PasswordField = PasswordHash };
app.AuthBootstrap = new AuthBootstrap {
Role = AppRole.Authenticator,
Login = Login,
Signup = Signup,
LoginPage = LoginPage,
};Both Login and Signup are marked and wired — the pairing the compiler insists on. Signup hashes the password
(never storing the plaintext), grants the ordinary Member role, and returns a ticket, so signing up is signing in.
For the variant where the first account bootstraps an admin, see role grants (and the first admin).
See also#
- auth bootstrap (login, before anyone is signed in) — the wiring an
[AuthMethod]must appear in, and the ephemeral principal it runs as - Security.* — hashing, verifying, tickets, random ids —
HashPassword·VerifyPassword·IssueJwt - role grants (and the first admin) — granting a role during signup without opening a path to self-elevation
- security { } — the
security { }grants that leash the auth role - app.Auth — how the platform authenticates a user of your app —
app.Auth, the other way a user of your app can be authenticated