Authentication
Like RBAC and Logger, Authentication has no
__invoke() — it isn't registered via ->middleware(). You construct it
once and call its methods directly from a login controller. Clarity owns the mechanism (hashing,
session lifecycle, throttling, tokens); your app owns the user table and supplies two lookup
closures.
Wiring it to your user table
final class Authentication extends \Monad\Clarity\Middlewares\Authentication
{
public function __construct()
{
parent::__construct(
findByCredential: static function (string $email): ?array {
$row = DB::run('SELECT id, password_hash, locked, email_verified_at FROM users WHERE email = ? LIMIT 1', [$email])->fetch();
return $row === false ? null : self::toResolverShape($row);
},
findById: static function (string $id): ?array { /* same shape, by id */ },
hmacSecret: (string) getenv('APP_SECRET'),
loginRateLimiter: new RateLimiter(maxAttempts: 5, windowSeconds: 300),
);
}
}
// Each closure returns array{id, passwordHash, locked, emailVerifiedAt}|null.
Logging in
$result = $auth->attempt($email, $password, $request->ip(), $request->userAgent() ?? '');
if (!$result->success) {
// $result->failureReason: Authentication::FAILURE_INVALID_CREDENTIALS
// | FAILURE_ACCOUNT_LOCKED | FAILURE_RATE_LIMITED
}
// $result->userId, $result->sessionId, $result->sessionToken, $result->needsRehash
Throttled per identifier before the password is even checked, and Hash::verify() always
runs exactly once — against the real hash or a fixed dummy one — so response time never leaks
whether an identifier matched a real account.
Password rehashing
$result->needsRehash is true when the stored hash was made with older parameters.
Persist a fresh hash with Utils\Hash::make() yourself — Authentication
never writes to your user table.
Remember-me
$rememberToken = $auth->issueRememberToken($userId, $request->ip(), $request->userAgent() ?? '');
// deliver as your own cookie name — Session::COOKIE_NAME ('mid') is reserved for the regular session
$result = $auth->resumeFromRememberToken($rememberToken, $request->ip(), $request->userAgent() ?? '');
// null if invalid/expired/revoked, or if the user has since been locked
Email verification and password reset
$token = $auth->issueEmailVerificationToken($userId); // stateless, HMAC-signed, no storage
$userId = $auth->verifyEmailVerificationToken($token); // ?string — null if invalid/expired
$token = $auth->issuePasswordResetToken($userId);
$userId = $auth->verifyPasswordResetToken($token);
Google SSO
$profile = $auth->verifyGoogleAuthorizationCode($code, $redirectUri);
// ['googleId' => ..., 'email' => ..., 'emailVerified' => bool, 'name' => ?string]
// Your app decides whether this maps to an existing user or creates one, then calls
// $auth->login($resolvedUserId, ...) directly — Authentication never touches your user table.
Next steps
- Rate Limiter — required for login throttling.
- Session — what
login()actually creates. - RBAC — checking permissions once a user is authenticated.