fix: answer /.well-known OAuth discovery without a canonical redirect; smoke checks the real path

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 16:27:25 +02:00
co-authored by Claude Opus 5.5
parent d67cd0ede6
commit bb85868192
4 changed files with 87 additions and 8 deletions
+10 -8
View File
@@ -60,14 +60,16 @@ curl -s --max-time 20 -H "Host: $HOST" -H 'X-Forwarded-Proto: https' \
|| fail "authorization-server metadata missing CIMD flag"
pass "RFC 8414 authorization-server metadata advertises CIMD"
# ENVIRONMENT check (never fails): the real .well-known path, same Host/proto.
WELLKNOWN=$(curl -s --max-time 20 -H "Host: $HOST" -H 'X-Forwarded-Proto: https' \
'http://localhost/.well-known/oauth-protected-resource')
if echo "$WELLKNOWN" | grep -q '"resource"'; then
pass "/.well-known/ reachable"
else
echo "WARN - /.well-known/ blocked by web server config (claude.ai discovery will fail; allow .well-known in nginx)"
fi
# The real .well-known path, same Host/proto, WITHOUT following redirects: clients use
# the exact resource_metadata URL from the 401 header. Needs the web server to pass
# /.well-known/ to WordPress (DDEV: .ddev/nginx/well-known.conf) and the plugin's
# DiscoveryCanonical (no trailing-slash 301).
for DOC in oauth-protected-resource oauth-authorization-server; do
CODE=$(curl -s --max-time 20 -o /dev/null -w '%{http_code}' -H "Host: $HOST" -H 'X-Forwarded-Proto: https' \
"http://localhost/.well-known/$DOC")
[ "$CODE" = "200" ] || fail "/.well-known/$DOC answered $CODE (web server blocks it or WordPress redirects)"
done
pass "/.well-known/ discovery documents answer directly (200, no redirect)"
# 2. Unauthenticated request -> 401 with WWW-Authenticate
UNAUTH=$(curl -s --max-time 20 -i -X POST "$ENDPOINT" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}')
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace LIT\MCP\Tests\Unit\Server;
use Brain\Monkey\Functions;
use LIT\MCP\Server\DiscoveryCanonical;
use LIT\MCP\Tests\Unit\TestCase;
final class DiscoveryCanonicalTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Functions\when('wp_parse_url')->alias('parse_url');
}
/** @return array<string, array{string}> */
public static function discoveryUrls(): array
{
return [
'protected resource' => ['https://example.test/.well-known/oauth-protected-resource'],
'authorization server' => ['https://example.test/.well-known/oauth-authorization-server'],
'subdirectory install' => ['https://example.test/wp/.well-known/oauth-protected-resource'],
];
}
/** @dataProvider discoveryUrls */
public function testDiscoveryDocumentsAreNeverRedirected(string $requested): void
{
self::assertFalse((new DiscoveryCanonical())->filter($requested . '/', $requested));
}
public function testOtherRedirectsStayUntouched(): void
{
$filter = new DiscoveryCanonical();
$page = 'https://example.test/seite';
$acme = 'https://example.test/.well-known/acme-challenge/x';
self::assertSame($page . '/', $filter->filter($page . '/', $page));
self::assertSame($acme . '/', $filter->filter($acme . '/', $acme));
self::assertFalse($filter->filter(false, 'https://example.test/.well-known/oauth-authorization-server'));
}
}