Auth
Stateless JWT authentication + attribute-driven authorization for any PSR-7/15 stack.
The package bundles three logical layers:
Layer | What it handles |
---|---|
Authentication | Login, password hashing/verification, JWT minting & refresh |
Authorization | Policy classes, role/permission checks, per-route guards |
Middleware | JwtAuthMiddleware (extracts/validates token) + AuthorizationMiddleware (enforces #[Can] rules) |
It relies only on the official firebase/php-jwt helper—no heavyweight security frameworks.
1 · Installation
composer require monkeyscloud/monkeyslegion-auth
This pulls in firebase/php-jwt ^6.0 and autoloads everything under MonkeysLegion\Auth\.
2 · Password Hasher
use MonkeysLegion\Auth\PasswordHasher;
$hash = PasswordHasher::hash('super-secret'); // bcrypt by default
$isOk = PasswordHasher::verify('super-secret', $hash);
Uses password_hash() / password_verify() behind the scenes.
Algorithm & cost are tweakable via static setters (or DI config).
3 · JWT Service
use MonkeysLegion\Auth\JwtService;
$jwt = new JwtService(
key: $_ENV['JWT_SECRET'],
issuer: 'monkeys.app',
expiresIn: 3600 // seconds
);
$token = $jwt->issue(['sub' => $userId, 'role' => 'admin']); // header.payload.signature
$claims = $jwt->parse($token); // array ← verified & decoded
Symmetric by default; pass an RSA keypair if you prefer asymmetric signing.
Built on firebase/php-jwt.
4 · Login / AuthService
use MonkeysLegion\Auth\AuthService;
$auth = new AuthService($users, $hasher, $jwtService);
try {
[$token, $claims] = $auth->attempt($email, $password);
} catch (AuthException $e) {
// wrong creds → 401
}
$users is any repository implementing findByEmail() + findById().
attempt() returns [jwtString, claimsArray] on success.
5 · JWT Auth Middleware
use MonkeysLegion\Auth\JwtAuthMiddleware;
$kernel->add(new JwtAuthMiddleware(
jwt: $jwtService,
header: 'Authorization', // "Bearer <token>"
cookie: 'ml_token', // optional fallback
passthrough: ['/login','/docs'] // no token required
));
If the token is missing or invalid → 401 JSON response.
On success, the decoded claims are injected as $request->getAttribute('user').
6 · Authorization (Policies + #[Can])
6.1 Defining a Policy
namespace App\Policies;
use App\Entity\Post;
use Psr\Http\Message\ServerRequestInterface;
final class PostPolicy implements PolicyInterface
{
public function edit(ServerRequestInterface $req, Post $post): bool
{
$user = $req->getAttribute('user');
return $user['id'] === $post->author_id || $user['role'] === 'admin';
}
}
Register policies in the DI container or via a simple array:
$authz->register(Post::class, PostPolicy::class);
6.2 Guarding a Controller method
use MonkeysLegion\Auth\Attributes\Can;
#[Can('edit', App\Entity\Post::class)]
public function edit(ServerRequestInterface $req): ResponseInterface
{
$post = $posts->find($req->getAttribute('route')['id']);
// … handle edit
}
AuthorizationMiddleware inspects the attribute, calls the relevant policy
method, and—if it returns false—short-circuits with 403 Forbidden.
7 · Authorization Middleware
use MonkeysLegion\Auth\AuthorizationMiddleware;
$kernel->add(new AuthorizationMiddleware(
service: $authz, // AuthorizationService instance
onFail: fn() => new JsonResponse(['error'=>'Forbidden'], 403),
));
Place it after JwtAuthMiddleware so user claims are available.
8 · Putting It All Together
$kernel = new MiddlewareDispatcher();
$kernel->add(new JwtAuthMiddleware($jwt));
$kernel->add(new AuthorizationMiddleware($authz));
$kernel->add($router->getMiddleware()); // routes with #[Can] guards
You now have secure login, stateless JWT sessions, and fine-grained
policy checks—all in ~40 lines of bootstrap code.
9 · CLI Goodies
# generate a new RSA keypair
php vendor/bin/ml auth:keygen --out config/jwt.pem --public-out public/jwt.pub
# hash a password for fixtures / tests
php vendor/bin/ml auth:hash "secret"
License
MIT © 2025 MonkeysCloud