-
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).
- Quick Start
- AuthManager ??Multi-Guard
- Guards
- Authorization Gate
- Middleware
- Password Hashing
- Contracts
- API Reference
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 |