Slim 4 + SQLite todo-list API providing email/password registration, login, and an authenticated GET /me endpoint. Stateless HS256 JWTs, bcrypt password hashing, uniform JSON error envelope, and a SQL migration runner. Includes PHPUnit feature tests and stage-1 docs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
50 lines
1.5 KiB
PHP
50 lines
1.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Auth;
|
|
|
|
use App\Exception\ApiException;
|
|
use App\Repository\UserRepository;
|
|
use Psr\Http\Message\ResponseInterface as Response;
|
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
use Psr\Http\Server\MiddlewareInterface;
|
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
|
use Throwable;
|
|
|
|
/**
|
|
* Requires a valid `Authorization: Bearer <jwt>` header. On success the resolved
|
|
* user row is attached to the request as the `user` attribute.
|
|
*/
|
|
final class AuthMiddleware implements MiddlewareInterface
|
|
{
|
|
public function __construct(
|
|
private readonly JwtService $jwt,
|
|
private readonly UserRepository $users,
|
|
) {
|
|
}
|
|
|
|
public function process(Request $request, RequestHandler $handler): Response
|
|
{
|
|
$header = $request->getHeaderLine('Authorization');
|
|
|
|
if (preg_match('/^Bearer\s+(\S+)$/i', $header, $matches) !== 1) {
|
|
throw new ApiException('Missing or malformed Authorization header.', 401);
|
|
}
|
|
|
|
try {
|
|
$claims = $this->jwt->verify($matches[1]);
|
|
} catch (Throwable) {
|
|
throw new ApiException('The access token is invalid or has expired.', 401);
|
|
}
|
|
|
|
$user = $this->users->findById((int) ($claims['sub'] ?? 0));
|
|
|
|
if ($user === null) {
|
|
throw new ApiException('The account for this token no longer exists.', 401);
|
|
}
|
|
|
|
return $handler->handle($request->withAttribute('user', $user));
|
|
}
|
|
}
|