Files
janniclas.loosenandClaude Opus 5.5 4d4bf57c9f fix(security): refuse OAuth authorize/consent for users without access (audit L1)
mcp-oauth has no filter for this, so AuthorizeGate runs on
template_redirect before its router and stops the browser endpoints
(authorize, authorize-callback, consent) for logged-in users without
lit_mcp_use with "Dein Benutzer hat keinen Zugriff auf LIT MCP." (403).
Login redirect, token and revoke stay mcp-oauth's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-26 19:30:54 +02:00

80 lines
2.7 KiB
PHP

<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Server;
use Brain\Monkey\Actions;
use Brain\Monkey\Functions;
use LIT\MCP\Access;
use LIT\MCP\Server\AuthorizeGate;
use LIT\MCP\Tests\Unit\TestCase;
use Mockery;
/**
* Security audit L1: a logged-in user without an allowed role must not complete mcp-oauth's
* authorize/consent flow (no application password, tokens or sessions for them).
*/
final class AuthorizeGateTest extends TestCase
{
private function given(string $endpoint, bool $loggedIn, bool $allowed): void
{
Functions\when('get_query_var')->alias(
static fn (string $var, $default = '') => $var === 'mcp_oauth_endpoint' ? $endpoint : $default
);
Functions\when('is_user_logged_in')->justReturn($loggedIn);
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap === Access::CAPABILITY && $allowed);
Functions\when('__')->returnArg();
Functions\when('esc_html')->returnArg();
}
public function testRunsBeforeMcpOauthsOwnHandler(): void
{
Actions\expectAdded('template_redirect')->once()->with(Mockery::type('array'), 1);
(new AuthorizeGate())->register();
}
/** @return array<string, array{string}> */
public static function userFacingEndpoints(): array
{
return ['authorize' => ['authorize'], 'authorize-callback' => ['authorize-callback'], 'consent' => ['consent']];
}
/** @dataProvider userFacingEndpoints */
public function testUsersWithoutAccessAreStoppedWithAClearMessage(string $endpoint): void
{
$this->given($endpoint, true, false);
Functions\expect('wp_die')->once()->with(
'Dein Benutzer hat keinen Zugriff auf LIT MCP.',
Mockery::type('string'),
Mockery::on(static fn (array $args): bool => ($args['response'] ?? null) === 403)
);
(new AuthorizeGate())->check();
}
public function testAllowedUsersLoginAndOtherEndpointsPass(): void
{
Functions\expect('wp_die')->never();
$this->given('consent', true, true);
(new AuthorizeGate())->check();
$this->given('authorize', false, false); // mcp-oauth sends them to the login first
(new AuthorizeGate())->check();
foreach (['token', 'revoke', ''] as $endpoint) { // no browser session there
$this->given($endpoint, true, false);
(new AuthorizeGate())->check();
}
}
public function testQueryVarMatchesMcpOauth(): void
{
$gate = new \ReflectionClass(AuthorizeGate::class);
self::assertSame(\WPMedia\MCP\OAuth\Auth\Rewrite::OAUTH_QUERY_VAR, $gate->getConstant('QUERY_VAR'));
}
}