Protect a route
You'll put a page behind a sign-in, send an anonymous visit to the sign-in page and read who the visitor is from a loader. The check is one file and it runs before any of your routes do.
Before you start
Install from crates.io. You need no Node and no package manager.
cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version
Every command and screenshot on this page was captured with fsr 0.x.
Session and identity#
A session is the visitor's scratch space: a cart, a draft, a preference. Every visitor has one, signed in or not. You declare its shape in app/schemas/session.ts and a body reads and writes it as a plain object.
An identity is who the visitor is: a subject and a map of claims. A body reads identity.subject and identity.claims.<name>; it reads null when nobody is signed in. You never assign to it. It arrives on the session when a login finishes.
Don't keep a
user_idin the session yourself, since any code path could set it. Readidentity, which only the login flow sets.
Write the check in middleware#
app/middleware.ts at the top of the app runs before every request that is not a static file: pages, the action route and handlers alike. It answers with a Response or with nothing.
;
Returning nothing lets the request continue to its route. A Response of its own ends the request there, so an anonymous visit to /account is sent to /auth/login, which starts a sign-in and comes back to return_to when it finishes. Middleware can also await next() to run the route and change its Response before returning it.
Put the check in middleware instead of in each loader. A route you add under /account next month is then covered the day you add it. The action route is covered too, so a protected page's actions cannot be called by a visitor who was refused the page.
Reading the identity in a loader#
;
The middleware has already turned anonymous visits away, so identity is set here. A claim from the account, such as identity.claims.role, reads the same way.
The host carries the identity onto outbound service calls on its own, so a service that needs to know who is asking is already told. Pass identity.subject as an argument only to a method that takes it as data rather than as who the caller is.
Prerendering#
identityis per request, so a route whose loader reads it renders per request. A page behind a sign-in can't be prerendered and the build won't try.
Configure the login#
Add an [auth] section to the configuration:
[]
= "file"
= "/sign-in"That mounts four routes the framework owns, the way it owns the action route. GET /auth/login starts a sign-in, taking return_to from the query, else the Referer's path, else /. The target is always a path on this origin. /auth/callback takes each answer, as a form post, a JSON post or a GET carrying an identity provider's query. A success is a 303 to where the sign-in began. A refusal is a 303 back to the login page with error=denied. POST /auth/enrol adds a method, such as a passkey, to whoever is signed in. POST /auth/logout verifies _csrf and clears identity and custody.
The file provider is DevProvider over config/auth.toml beside app.toml: a list of accounts with passwords and optional claims.
[[]]
= "alice"
= "wonder"
= { = "admin" }It exists so the application can be written against identity from the first day and swap in a real provider later without a body changing, because a body only ever sees identity.
Write the sign-in page#
Auth never renders anything, so [auth] login names a route you write. For the file provider it is an ordinary form posting user and password to /auth/callback:
;
;
Run fsr dev app and open /account. You land on /sign-in. Sign in as alice with wonder and you are back on /account.
Log out#
Signing out is a form that posts to /auth/logout with the CSRF token. Put it on the account page, which reads csrf_token like any other prop:
;
Clearing the identity clears the tokens held beside it, since the two cells persist together. Only the auth routes change the identity. A body can't set or clear it. Signing out sends you to /. Opening /account again sends you back to the sign-in page.
Next up: 355. Sign in with a passkey.