docs / Auth & sessions
Auth & sessions
Auth is compiled, not delegated: the session guard, the CSRF middleware and the throttle run on the Workers kernel. The one thing that cannot be compiled is a guard driver your app defines in PHP.
The session guard
| surface | verdict |
|---|---|
| Auth::attempt / login / logout | compiled, byte-parity with PHP 8.3 (13 guard runs) |
| remember-me, recaller cookie | compiled, with token rotation |
| EloquentUserProvider | compiled against your model |
| BcryptHasher | bcryptjs with $2y$ ⇄ $2b$ parity both ways |
| auth, guest, auth.basic middleware | compiled, including 401 JSON vs redirect |
| Gate::allows / denies | policies compiled at build time; the before() hook honoured |
| Gate::authorize / any / define | fails loud — use allows()/denies(), or a port |
Session and CSRF
Sessions live in D1 (the database driver) and carry flash data, rotation,migrate/regenerate/invalidate, the token plumbing and thePreventRequestForgery middleware. @csrf and @method work inside compiled views; the token is compared in constant time like Laravel does.
Cookie encryption is the Encrypter parity path: AES-256-CBC with an HMAC, over WebCrypto, using the same key derivation and serialization as PHP. A cookie minted by Laravel opens at the edge and the reverse — which is what lets you run both during a migration.
Throttling
Numeric throttle:n[,m] is wired over the D1-backed database cache store, with the same counter semantics as Laravel. A named rate limiter needs itsAppServiceProvider definition, which the build cannot run — it fails loud and asks you to write the numbers inline.
When your app registers its own guard
Auth::extend('seal', …) is a PHP closure. The compiled rung cannot run it, and silently answering “no user” would let a guarded route through — so the build refuses, at config level, before any route is compiled. The way out is a guard port: declare the driver and provide user(ctx).
// registry.json → "guards": { "seal": "seal-guard" }
// ports/seal-guard.mjs
export default {
async user(ctx) {
const id = await readToken(ctx.request, ctx.app.env.SEAL_KEY);
return id === null ? null : ctx.app.models.User.find(ctx.app.db, id);
},
};| rule | why |
|---|---|
the module exports user(ctx) | required; a port without it is refused at build time |
throw on failure, return null for a guest | a throwing port keeps throwing: a failure never degrades into “no user” |
call via ctx.app.* | bindings are ctx.app.env, ctx.app.db, ctx.app.models — never at the top level |
| once per guard per request | PortedGuard memoises; the memo drops on setRequest() |
Only the driver is yours. auth:seal still resolves through the compiledAuthenticate middleware; auth()->user(), theguest middleware, the 401/redirect and Auth::viaRemember stay compiled.
Source: README.md §Compiles today, §A guard port; COMPILED.md; README §Auth row — repository ba1e9a5