Skip to content

AuthManager

Ray Fung edited this page Feb 26, 2026 · 3 revisions

AuthManager

Razy provides a multi-guard authentication system with an authorization gate, middleware support, and pluggable guard implementations. The system supports multiple authentication strategies simultaneously (e.g. session-based for web, token-based for API).


Table of Contents


Quick Start

use Razy\Auth\AuthManager;
use Razy\Auth\CallbackGuard;
use Razy\Auth\GenericUser;

// Create auth manager
$auth = new AuthManager(defaultGuard: 'web');

// Register a session-based guard
$auth->addGuard('web', new CallbackGuard(
    userResolver: function () {
        $userId = $_SESSION['user_id'] ?? null;
        if ($userId) {
            return new GenericUser(['id' => $userId, 'name' => 'John']);
        }
        return null;
    },
    credentialValidator: function (array $credentials) {
        return $credentials['password'] === 'secret';
    }
));

// Check authentication
if ($auth->check()) {
    $user = $auth->user();
    echo "Welcome, " . $user->getAttribute('name');
}

AuthManager ??Multi-Guard

The AuthManager manages multiple named guards and proxies common auth methods to the default guard:

use Razy\Auth\AuthManager;

$auth = new AuthManager(defaultGuard: 'web');

// Register guards
$auth->addGuard('web', $sessionGuard);
$auth->addGuard('api', $tokenGuard);

// Use default guard
$auth->check();          // is authenticated?
$auth->guest();          // is guest?
$auth->user();           // get authenticated user or null
$auth->id();             // get user identifier
$auth->validate($creds); // validate credentials
$auth->setUser($user);   // manually set user

// Use specific guard
$auth->guard('api')->check();
$auth->guard('api')->user();

// Inspect guards
$auth->hasGuard('api');         // true
$auth->getGuardNames();         // ['web', 'api']
$auth->getDefaultGuard();       // 'web'
$auth->setDefaultGuard('api');

Guards

CallbackGuard

A flexible guard that uses closures for user resolution and credential validation:

use Razy\Auth\CallbackGuard;
use Razy\Auth\GenericUser;

$guard = new CallbackGuard(
    userResolver: function () {
        // Resolve the authenticated user from session, token, etc.
        $token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
        if ($token && $userData = verifyToken($token)) {
            return new GenericUser($userData);
        }
        return null;
    },
    credentialValidator: function (array $credentials) {
        // Validate credentials (login attempt)
        $user = findUserByEmail($credentials['email']);
        return $user && password_verify($credentials['password'], $user['password_hash']);
    }
);

// Lazy resolution ??userResolver is called once on first user() call
$user = $guard->user();    // calls resolver
$user = $guard->user();    // returns cached result

// Reset for next request (worker mode)
$guard->reset();           // clears cached user, re-resolves on next call

Custom Guards

Implement GuardInterface for custom authentication strategies:

use Razy\Contract\GuardInterface;
use Razy\Contract\AuthenticatableInterface;

class JwtGuard implements GuardInterface
{
    private ?AuthenticatableInterface $user = null;

    public function check(): bool
    {
        return $this->user() !== null;
    }

    public function guest(): bool
    {
        return !$this->check();
    }

    public function user(): ?AuthenticatableInterface
    {
        if ($this->user === null) {
            $token = $this->extractBearerToken();
            if ($payload = $this->validateJwt($token)) {
                $this->user = new GenericUser($payload);
            }
        }
        return $this->user;
    }

    public function id(): string|int|null
    {
        return $this->user()?->getAuthIdentifier();
    }

    public function validate(array $credentials): bool
    {
        // Validate credentials without setting user
        return $this->attemptLogin($credentials);
    }

    public function setUser(AuthenticatableInterface $user): void
    {
        $this->user = $user;
    }

    // ... private helper methods
}

GenericUser

A simple AuthenticatableInterface implementation using an associative array:

use Razy\Auth\GenericUser;

$user = new GenericUser([
    'id'       => 42,
    'name'     => 'John Doe',
    'email'    => 'john@example.com',
    'password' => '$2y$10$...',
    'role'     => 'admin',
]);

$user->getAuthIdentifier();     // 42
$user->getAuthIdentifierName(); // 'id' (default)
$user->getAuthPassword();       // '$2y$10$...'
$user->getAttribute('name');    // 'John Doe'
$user->getAttribute('role');    // 'admin'
$user->hasAttribute('email');   // true
$user->getAttributes();         // full array

// Custom identifier name
$user = new GenericUser([
    '__identifier_name' => 'uuid',
    'uuid' => 'abc-123',
    'name' => 'Jane',
]);
$user->getAuthIdentifierName(); // 'uuid'
$user->getAuthIdentifier();     // 'abc-123'

Authorization Gate

The Gate class provides ability-based and policy-based authorization:

Defining Abilities

use Razy\Auth\Gate;

$gate = new Gate($auth);

// Define abilities with closures
$gate->define('edit-post', function (?AuthenticatableInterface $user, $post) {
    return $user && $user->getAuthIdentifier() === $post['author_id'];
});

$gate->define('delete-post', function (?AuthenticatableInterface $user, $post) {
    return $user && $user->getAttribute('role') === 'admin';
});

$gate->define('create-post', function (?AuthenticatableInterface $user) {
    return $user !== null; // any authenticated user
});

// Check abilities
$gate->allows('edit-post', $post);    // true/false
$gate->denies('edit-post', $post);    // true/false
$gate->has('edit-post');               // true (ability exists)

// Check multiple
$gate->check(['edit-post', 'delete-post'], $post);  // ALL must pass
$gate->any(['edit-post', 'delete-post'], $post);     // at least one
$gate->none(['edit-post', 'delete-post'], $post);    // none pass

// Authorize ??throws AccessDeniedException if denied
$gate->authorize('edit-post', $post);

// Check as specific user
$gate->forUser($anotherUser)->allows('edit-post', $post);

Policies

Map model classes to policy classes for organized authorization:

// Register policies
$gate->policy(Post::class, PostPolicy::class);
$gate->policy(Comment::class, CommentPolicy::class);

// Policy class ??methods match ability names (kebab ??camelCase)
class PostPolicy
{
    // 'edit-post' ??editPost()
    public function editPost(?AuthenticatableInterface $user, Post $post): bool
    {
        return $user && $user->getAuthIdentifier() === $post->authorId;
    }

    // 'delete-post' ??deletePost()
    public function deletePost(?AuthenticatableInterface $user, Post $post): bool
    {
        return $user && $user->getAttribute('role') === 'admin';
    }

    // 'create-post' ??createPost()
    public function createPost(?AuthenticatableInterface $user): bool
    {
        return $user !== null;
    }
}

// Usage
$gate->allows('edit-post', $post);   // invokes PostPolicy::editPost()

Interceptors

Before/after interceptors can override individual ability checks:

// Before ??runs BEFORE ability check; return non-null to override
$gate->before(function (?AuthenticatableInterface $user, string $ability) {
    // Super admin can do anything
    if ($user && $user->getAttribute('role') === 'superadmin') {
        return true; // bypass all checks
    }
    return null; // continue to normal check
});

// After ??runs AFTER ability check; can override the result
$gate->after(function (?AuthenticatableInterface $user, string $ability, bool $result) {
    // Log all authorization decisions
    logger()->info("Gate: {$ability} = " . ($result ? 'allowed' : 'denied'));
    return null; // return non-null to override $result
});

Middleware

AuthMiddleware

Require authentication for a route/controller:

use Razy\Auth\AuthMiddleware;

$middleware = new AuthMiddleware(
    auth: $authManager,
    guard: 'api',                    // null = default guard
    onUnauthorized: function () {    // custom 401 handler
        http_response_code(401);
        echo json_encode(['error' => 'Unauthorized']);
    }
);

// In a pipeline context
$result = $middleware->handle($context, function ($context) {
    // Only reached if authenticated
    return handleRequest($context);
});

AuthorizeMiddleware

Require a specific ability:

use Razy\Auth\AuthorizeMiddleware;

$middleware = new AuthorizeMiddleware(
    gate: $gate,
    ability: 'edit-post',
    argumentResolver: function (array $context) {
        return [$context['post']]; // arguments for the ability check
    },
    onForbidden: function () {
        http_response_code(403);
        echo json_encode(['error' => 'Forbidden']);
    }
);

Password Hashing

The Hash class provides a static utility for password hashing:

use Razy\Auth\Hash;

// Hash a password (bcrypt by default)
$hash = Hash::make('my-secret-password');
$hash = Hash::make('password', PASSWORD_ARGON2ID, ['memory_cost' => 65536]);

// Verify
$valid = Hash::check('my-secret-password', $hash); // true

// Check if rehash needed (algorithm/options changed)
if (Hash::needsRehash($hash)) {
    $newHash = Hash::make($plainTextPassword);
    // update stored hash
}

// Inspect hash info
$info = Hash::info($hash);
// ['algo' => 1, 'algoName' => 'bcrypt', 'options' => ['cost' => 10]]

Contracts

GuardInterface

interface GuardInterface {
    public function check(): bool;
    public function guest(): bool;
    public function user(): ?AuthenticatableInterface;
    public function id(): string|int|null;
    public function validate(array $credentials): bool;
    public function setUser(AuthenticatableInterface $user): void;
}

AuthenticatableInterface

interface AuthenticatableInterface {
    public function getAuthIdentifier(): string|int;
    public function getAuthIdentifierName(): string;
    public function getAuthPassword(): string;
}

API Reference

AuthManager

Method Signature Description
__construct (array $guards = [], string $defaultGuard = 'default') Create manager
addGuard (string $name, GuardInterface $guard): static Register guard
guard (?string $name = null): GuardInterface Get guard (throws on unknown)
setDefaultGuard (string $name): static Change default
getDefaultGuard (): string Get default name
hasGuard (string $name): bool Check guard exists
getGuardNames (): array All guard names
check (): bool Authenticated? (default guard)
guest (): bool Guest? (default guard)
user (): ?AuthenticatableInterface Get user (default guard)
id (): string|int|null Get user ID (default guard)
validate (array $credentials): bool Validate (default guard)
setUser (AuthenticatableInterface $user): void Set user (default guard)

Gate

Method Signature Description
define (string $ability, Closure $callback): static Define ability
policy (string $modelClass, string $policyClass): static Register policy
before (Closure $callback): static Before interceptor
after (Closure $callback): static After interceptor
allows (string $ability, mixed ...$args): bool Check allowed
denies (string $ability, mixed ...$args): bool Check denied
check (array $abilities, mixed ...$args): bool All must pass
any (array $abilities, mixed ...$args): bool At least one
none (array $abilities, mixed ...$args): bool None pass
authorize (string $ability, mixed ...$args): void Throws AccessDeniedException
forUser (AuthenticatableInterface $user): static Scoped clone
has (string $ability): bool Ability exists?

Hash

Method Signature Description
make (string $password, string|int|null $algo = PASSWORD_BCRYPT, array $options = []): string Hash password
check (string $password, string $hash): bool Verify password
needsRehash (string $hash, string|int|null $algo = PASSWORD_BCRYPT, array $options = []): bool Check rehash needed
info (string $hash): array Hash info

??Previous: Authenticator RateLimiter

Clone this wiki locally