Authentication
Protect the route with a guard, then read the authenticated user from the request:
Route.get('/profile', (Request request) { final user = request.requireUser<User>(); return {'id': user.id, 'email': user.email};}).middleware(['auth:cartouche']);Maat’s core package defines authentication contracts, password hashing,
guards, and request access. It does not define a User model or depend on the
ORM: the columns that identify a user belong to the application.
The Authenticatable Contract
Implement Authenticatable on any object a guard may return:
class User extends Model<User> implements Authenticatable { // Model definition and fields omitted.
@override Object get authIdentifier => id!;
@override String get authPassword => password;}authIdentifier is the value stored by token and session systems.
authPassword is the encoded password, or an empty string for an account that
cannot sign in with a password.
Password Hashing
final encoded = await Hash.makeAsync('correct horse battery staple');
if (await Hash.checkAsync(input, encoded)) { // Authenticated.}
if (Hash.needsRehash(encoded)) { // Replace it after a successful login.}Store only encoded, never the plaintext password. Treat both the submitted
password and the encoded hash as sensitive values that must not enter logs.
Use the asynchronous methods in request handlers. Argon2 is intentionally
expensive; makeAsync and checkAsync move that work to another isolate so
the HTTP isolate can keep serving requests. The synchronous make and check
methods remain useful in scripts and controlled tests.
Passwords use Argon2id because human-chosen input has low entropy and must be expensive to guess. Personal access tokens use SHA-256 instead: their random 40-character secret already has enough entropy, so making every authenticated request deliberately slow would add cost without useful protection.
Configure new hashes in config/hashing.dart:
Map<String, dynamic> get hashing => { 'argon': {'memory': 19456, 'time': 2, 'threads': 1},};The parameters used to verify an existing password come from its encoded PHC
string. Raising the configuration therefore does not invalidate old hashes;
needsRehash tells you when to upgrade one after a successful login.
Malformed hashes return false, not an exception.
Guards
A guard resolves the user for one request:
class HeaderGuard implements Guard { @override Future<Authenticatable?> user(Request request) async { final id = int.tryParse(request.header('x-user-id') ?? ''); return id == null ? null : User.query().find(id); }}Register guards from a service provider:
@overridevoid register() { Auth.extend('header', HeaderGuard.new);}The core package ships no concrete guard. cartouche registers
cartouche; a session package can later register web through the same
contract.
Protecting Routes
Register the parameterized middleware alias in bootstrap/app.dart:
.withMiddleware((middleware) => middleware.alias({ 'auth': Authenticate.factory,}))Then name the guard on a route:
Route.get('/profile', profile).middleware(['auth:cartouche']);With no parameter, auth reads the default from config/auth.dart:
import 'package:maat/maat.dart';
Map<String, dynamic> get auth => { 'defaults': {'guard': env('AUTH_GUARD', 'cartouche')},};An unauthenticated request receives 401. An unknown guard throws an
ArgumentError instead of silently leaving the route unprotected.
Reading the User
Future<Object?> profile(Request request) async { final user = request.requireUser<User>(); return user;}request.user<User>() returns null when no user was authenticated.
requireUser<User>() throws a 401 and is convenient behind auth middleware.
Both check the requested type; asking for the wrong model type produces an
error naming the mismatch rather than a misleading null.
Failure Responses
When a guard returns null, authentication stops before the handler and the
client receives 401 Unauthenticated. An unknown guard name throws an
ArgumentError; a typo such as auth:cartochue must fail loudly rather than
silently expose the route.
Authorization is a separate step. Once a user is authenticated, middleware or
application policy should still decide whether that user may perform the
requested action and return 403 when they may not.
Testing Authentication
Test the route boundary, not just the guard in isolation:
final guest = await TestClient(app).get('/profile');guest.assertStatus(401);
final member = await TestClient(app) .withToken(token) .get('/profile');member.assertOk();For Cartouche tests that do not need token persistence, use
Cartouche.actingAs(user) and reset its static state in tearDown. See
Cartouche API Tokens.