Daravelv1.0.0
ENID GitHub

Authentication and authorization

Guards are registered in bootstrap/app.dart:

.withAuth((auth) {
  auth.token<User>('token', ModelUserProvider<User>(User.definition));
  auth.jwt<User>('jwt', ModelUserProvider<User>(User.definition));
})

The model mixes in AuthenticatableModel. Auth.user(), Auth.id(), Auth.check() and Auth.guard('jwt') work anywhere inside a request, job or command; the auth (or auth:jwt) middleware answers 401 with {"message": "Unauthenticated."}.

Personal access tokens (Sanctum)#

final user = await Auth.retrieveByCredentials<User>({
  'email': email,
  'password': password,
});
if (user == null) {
  throw ValidationException.withMessages({'email': ['Invalid credentials.']});
}
final token = await Auth.issueToken(user, 'cli', abilities: ['posts:read']);
return {'token': token.plainTextToken, 'token_type': 'Bearer'};

Tokens use Sanctum 4's format and table: the client receives {id}|{40 random characters}{crc32b checksum}, the personal_access_tokens row stores the SHA-256 of everything after the |, and tokenable_type is the model's Laravel class (App\Models\User). A token created by Laravel authenticates in Daravel and the other way round. SANCTUM_TOKEN_PREFIX adds Sanctum's token_prefix; AUTH_TOKEN_EXPIRATION (minutes) sets a global lifetime. tokenCan, revokeCurrent, revokeAll and sanctum:prune-expired are available on Auth.tokenGuard().

When no account matches the e-mail, the password is still hashed against a dummy hash, so a failed login takes the same time whether or not the account exists.

JWT#

Auth.issueJwt(user) signs HS256 tokens with JWT_SECRET (or APP_KEY), or EdDSA with JWT_PUBLIC_KEY / JWT_PRIVATE_KEY. Verification rejects alg: none, algorithms without a configured key, bad signatures and expired tokens; a denylist supports logout.

Hashing#

Hash.make / Hash.check run in a compute isolate. Drivers: bcrypt (default, cost 12, $2y$ like Laravel), argon2id and pbkdf2. Hash.check detects the driver from the hash. With HASH_VERIFY=true (the default) it refuses foreign formats except the strong family (bcrypt and argon2id), so switching HASH_DRIVER between those two keeps every existing password valid while Hash.needsRehash and rehashPasswordIfRequired upgrade hashes at login. Keep bcrypt while a Laravel app shares the users table: Laravel verifies only bcrypt by default.

Encryption#

Crypt.encryptString / decryptString produce Laravel's payload (base64(json{iv, value, mac, tag})) keyed by APP_KEY. The cipher is APP_CIPHER: AES-256-CBC (the default, like Laravel; encrypt-then-MAC with HMAC-SHA256) or AES-256-GCM. Use the same value as any Laravel app you share data with. APP_PREVIOUS_KEYS enables key rotation. Crypt.encrypt JSON-encodes values (Laravel uses PHP serialize()), so only the string variants are cross-runtime.

Authorization#

.withPolicies((gate) {
  gate.define('publish', (user, args) => user is User && user.isEditor);
  gate.policy<Post>(const PostPolicy());
})

Gate.allows, Gate.denies, Gate.authorize (403 with This action is unauthorized.) and the can:update,post middleware work like Laravel's. Policy methods default to false.