79 lines
2.9 KiB
PHP
79 lines
2.9 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace TinkerTickets\Mcp\Auth;
|
||
|
|
|
||
|
|
use Psr\Http\Message\ResponseFactoryInterface;
|
||
|
|
use Psr\Http\Message\ResponseInterface;
|
||
|
|
use Psr\Http\Message\ServerRequestInterface;
|
||
|
|
use Psr\Http\Message\StreamFactoryInterface;
|
||
|
|
use Psr\Http\Server\MiddlewareInterface;
|
||
|
|
use Psr\Http\Server\RequestHandlerInterface;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Maps a validated access token to a Tinker Tickets user, applying exactly
|
||
|
|
* the same rules as the web login (AuthMiddleware): the shared
|
||
|
|
* AccessPolicy::hasAppAccess() group check, then
|
||
|
|
* UserModel::syncUserFromAuthelia() to create/update the user row and derive
|
||
|
|
* is_admin from groups.
|
||
|
|
*
|
||
|
|
* Must run after the SDK's AuthorizationMiddleware, which has already
|
||
|
|
* verified the token's signature, issuer, audience and expiry and attached
|
||
|
|
* its claims as `oauth.claims` / `oauth.scopes` request attributes.
|
||
|
|
*/
|
||
|
|
final class IdentityMiddleware implements MiddlewareInterface
|
||
|
|
{
|
||
|
|
public function __construct(
|
||
|
|
private readonly \mysqli $conn,
|
||
|
|
private readonly ResponseFactoryInterface $responseFactory,
|
||
|
|
private readonly StreamFactoryInterface $streamFactory,
|
||
|
|
) {
|
||
|
|
}
|
||
|
|
|
||
|
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||
|
|
{
|
||
|
|
$claims = $request->getAttribute('oauth.claims');
|
||
|
|
$scopes = $request->getAttribute('oauth.scopes') ?? [];
|
||
|
|
if (!is_array($claims)) {
|
||
|
|
// Only reachable if the stack is misordered; never serve anonymously.
|
||
|
|
return $this->deny(500, 'Identity unavailable');
|
||
|
|
}
|
||
|
|
|
||
|
|
$username = is_string($claims['preferred_username'] ?? null) ? trim($claims['preferred_username']) : '';
|
||
|
|
if ($username === '') {
|
||
|
|
return $this->deny(403, 'Access token has no preferred_username claim');
|
||
|
|
}
|
||
|
|
|
||
|
|
$groupsClaim = $claims['groups'] ?? [];
|
||
|
|
$groups = implode(',', array_filter(
|
||
|
|
is_array($groupsClaim) ? $groupsClaim : [$groupsClaim],
|
||
|
|
'is_string'
|
||
|
|
));
|
||
|
|
|
||
|
|
if (!\AccessPolicy::hasAppAccess($groups)) {
|
||
|
|
return $this->deny(403, 'Your account is not permitted to use Tinker Tickets');
|
||
|
|
}
|
||
|
|
|
||
|
|
$user = (new \UserModel($this->conn))->syncUserFromAuthelia(
|
||
|
|
$username,
|
||
|
|
is_string($claims['name'] ?? null) ? $claims['name'] : '',
|
||
|
|
is_string($claims['email'] ?? null) ? $claims['email'] : '',
|
||
|
|
$groups
|
||
|
|
);
|
||
|
|
|
||
|
|
McpIdentity::set($user, array_values(array_filter($scopes, 'is_string')));
|
||
|
|
|
||
|
|
return $handler->handle($request);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function deny(int $status, string $message): ResponseInterface
|
||
|
|
{
|
||
|
|
return $this->responseFactory->createResponse($status)
|
||
|
|
->withHeader('Content-Type', 'application/json')
|
||
|
|
->withBody($this->streamFactory->createStream(json_encode([
|
||
|
|
'jsonrpc' => '2.0',
|
||
|
|
'id' => null,
|
||
|
|
'error' => ['code' => -32001, 'message' => $message],
|
||
|
|
])));
|
||
|
|
}
|
||
|
|
}
|