-
Notifications
You must be signed in to change notification settings - Fork 0
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).
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');
}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');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 callImplement 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
}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'The Gate class provides ability-based and policy-based authorization:
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);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()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
});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);
});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']);
}
);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]]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;
}interface AuthenticatableInterface {
public function getAuthIdentifier(): string|int;
public function getAuthIdentifierName(): string;
public function getAuthPassword(): string;
}| 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) |
| 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? |
| 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 |