Initial commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 02:40:42 +03:00
commit 98a4094c5e
53 changed files with 2633 additions and 0 deletions

10
src/Auth/AuthContext.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace Pronchev\Pinecore\Auth;
final class AuthContext
{
public function __construct(
public readonly object $user,
) {}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Pronchev\Pinecore\Auth;
use RuntimeException;
final class AuthException extends RuntimeException {}

View File

@@ -0,0 +1,38 @@
<?php
namespace Pronchev\Pinecore\Auth;
use Pronchev\Pinecore\Http\MiddlewareInterface;
use Pronchev\Pinecore\Http\Request;
final class AuthMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly JwtService $jwt,
private readonly UserProviderInterface $users,
) {}
/**
* Resolves the JWT from the Authorization header and injects AuthContext into the request.
*
* @throws AuthException if the token is missing, invalid, or the user does not exist
*/
public function process(Request $request): Request
{
$header = $request->headers['authorization'] ?? '';
if (!str_starts_with($header, 'Bearer ')) {
throw new AuthException('Missing or malformed Authorization header');
}
$tokenString = substr($header, 7);
$userId = $this->jwt->verify($tokenString);
$user = $this->users->findById($userId);
if ($user === null) {
throw new AuthException('User not found');
}
return $request->withContext('auth', new AuthContext($user));
}
}

69
src/Auth/JwtService.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
namespace Pronchev\Pinecore\Auth;
use DateTimeImmutable;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\StrictValidAt;
use Pronchev\Pinecore\Config;
use Psr\Clock\ClockInterface;
final class JwtService
{
private readonly Configuration $jwtConfig;
private readonly int $accessTtl;
public function __construct(Config $config)
{
$secret = $config->get('jwt.secret');
if ($secret === '') {
throw new \RuntimeException('JWT_SECRET is not configured');
}
$this->accessTtl = $config->get('jwt.access_ttl');
$signer = new Sha256();
$key = InMemory::plainText($secret);
$this->jwtConfig = Configuration::forSymmetricSigner($signer, $key);
$this->jwtConfig->setValidationConstraints(
new SignedWith($signer, $key),
new StrictValidAt(new class implements ClockInterface {
public function now(): DateTimeImmutable
{
return new DateTimeImmutable();
}
}),
);
}
public function issue(string $userId): string
{
$now = new DateTimeImmutable();
$token = $this->jwtConfig->builder()
->issuedAt($now)
->expiresAt($now->modify("+{$this->accessTtl} seconds"))
->relatedTo($userId)
->getToken($this->jwtConfig->signer(), $this->jwtConfig->signingKey());
return $token->toString();
}
public function verify(string $tokenString): string
{
try {
$token = $this->jwtConfig->parser()->parse($tokenString);
} catch (\Throwable) {
throw new AuthException('Invalid token');
}
if (!$this->jwtConfig->validator()->validate($token, ...$this->jwtConfig->validationConstraints())) {
throw new AuthException('Token validation failed');
}
return $token->claims()->get('sub');
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Pronchev\Pinecore\Auth;
interface UserProviderInterface
{
public function findById(string $id): ?object;
}