Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Security

[AuthMethod] — a function an unauthenticated visitor may call

[AuthMethod] string Login(string email, string password) { … }

`[AuthMethod]` marks a sign-in function — login, signup, password-reset — as reachable by a visitor who is not signed in. Everything else in your app refuses an unauthenticated caller before it runs, which is what you want; sign-in is the one flow that cannot require the thing it produces. The marker is checked against `app.AuthBootstrap` **both ways**: a marked function must be wired, and a wired function must be marked. So an anonymous entry point cannot exist by accident.

stable3 examples compiled by CIsecurityauthentication

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.AuthBootstrap

A 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 wroteWhat happensWhy
[AuthMethod] on a function not wired in app.AuthBootstrapcompile errorIt 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 errorThe signed-out visitor would be refused before your login could run — a login page that can never log anyone in.
Bothit worksThe 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#

Related

auth bootstrap (login, before anyone is signed in)

Under deny-all, login faces a paradox: it must read a user row *before* anyone is authenticated. `app.AuthBootstrap`…

role grants (and the first admin)

A role is granted by an ordinary entity — any entity that has both a reference to your `[Principal]` and a property…

security { }

The rules that decide who may read and write an entity's rows. A where clause filters by the row (the owner sees their…

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

The calls an authentication flow needs: `HashPassword` (salted, one-way), `VerifyPassword` (constant-work comparison…