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.