Skip to content

Commit 443af5c

Browse files
committed
feat: add LenientOidcDiscoveryMetadataPolicy for IdPs without code_challenge_methods_supported
Some identity providers (e.g. FusionAuth, Microsoft Entra ID) omit code_challenge_methods_supported from their OIDC discovery response despite supporting PKCE with S256. This policy relaxes the validation to only require authorization_endpoint, token_endpoint, and jwks_uri.
1 parent e8f5a8b commit 443af5c

2 files changed

Lines changed: 136 additions & 0 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Server\Transport\Http\OAuth;
13+
14+
/**
15+
* Lenient metadata policy for identity providers that do not yet include
16+
* code_challenge_methods_supported in their OIDC discovery response.
17+
*
18+
* While the MCP specification requires authorization servers to advertise
19+
* code_challenge_methods_supported, some providers (e.g. FusionAuth,
20+
* Microsoft Entra ID) omit this field despite supporting PKCE with S256.
21+
*
22+
* Use this policy as a pragmatic workaround until those providers update
23+
* their discovery metadata.
24+
*/
25+
final class LenientOidcDiscoveryMetadataPolicy implements OidcDiscoveryMetadataPolicyInterface
26+
{
27+
public function isValid(mixed $metadata): bool
28+
{
29+
return \is_array($metadata)
30+
&& isset($metadata['authorization_endpoint'], $metadata['token_endpoint'], $metadata['jwks_uri'])
31+
&& \is_string($metadata['authorization_endpoint'])
32+
&& '' !== trim($metadata['authorization_endpoint'])
33+
&& \is_string($metadata['token_endpoint'])
34+
&& '' !== trim($metadata['token_endpoint'])
35+
&& \is_string($metadata['jwks_uri'])
36+
&& '' !== trim($metadata['jwks_uri']);
37+
}
38+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Tests\Unit\Server\Transport\Http\OAuth;
13+
14+
use Mcp\Server\Transport\Http\OAuth\LenientOidcDiscoveryMetadataPolicy;
15+
use PHPUnit\Framework\Attributes\DataProvider;
16+
use PHPUnit\Framework\Attributes\TestDox;
17+
use PHPUnit\Framework\TestCase;
18+
19+
class LenientOidcDiscoveryMetadataPolicyTest extends TestCase
20+
{
21+
#[DataProvider('provideValidMetadata')]
22+
#[TestDox('valid metadata: $description')]
23+
public function testValidMetadata(mixed $metadata, string $description): void
24+
{
25+
$policy = new LenientOidcDiscoveryMetadataPolicy();
26+
27+
$this->assertTrue($policy->isValid($metadata));
28+
}
29+
30+
/**
31+
* @return iterable<string, array{mixed, string}>
32+
*/
33+
public static function provideValidMetadata(): iterable
34+
{
35+
$base = [
36+
'authorization_endpoint' => 'https://auth.example.com/authorize',
37+
'token_endpoint' => 'https://auth.example.com/token',
38+
'jwks_uri' => 'https://auth.example.com/jwks',
39+
];
40+
41+
yield 'without code_challenge_methods_supported' => [$base, 'without code_challenge_methods_supported'];
42+
43+
yield 'with code_challenge_methods_supported' => [
44+
$base + ['code_challenge_methods_supported' => ['S256']],
45+
'with code_challenge_methods_supported',
46+
];
47+
}
48+
49+
#[DataProvider('provideInvalidMetadata')]
50+
#[TestDox('invalid metadata: $description')]
51+
public function testInvalidMetadata(mixed $metadata, string $description): void
52+
{
53+
$policy = new LenientOidcDiscoveryMetadataPolicy();
54+
55+
$this->assertFalse($policy->isValid($metadata));
56+
}
57+
58+
/**
59+
* @return iterable<string, array{mixed, string}>
60+
*/
61+
public static function provideInvalidMetadata(): iterable
62+
{
63+
yield 'missing authorization_endpoint' => [
64+
[
65+
'token_endpoint' => 'https://auth.example.com/token',
66+
'jwks_uri' => 'https://auth.example.com/jwks',
67+
],
68+
'missing authorization_endpoint',
69+
];
70+
71+
yield 'missing token_endpoint' => [
72+
[
73+
'authorization_endpoint' => 'https://auth.example.com/authorize',
74+
'jwks_uri' => 'https://auth.example.com/jwks',
75+
],
76+
'missing token_endpoint',
77+
];
78+
79+
yield 'missing jwks_uri' => [
80+
[
81+
'authorization_endpoint' => 'https://auth.example.com/authorize',
82+
'token_endpoint' => 'https://auth.example.com/token',
83+
],
84+
'missing jwks_uri',
85+
];
86+
87+
yield 'empty endpoint string' => [
88+
[
89+
'authorization_endpoint' => '',
90+
'token_endpoint' => 'https://auth.example.com/token',
91+
'jwks_uri' => 'https://auth.example.com/jwks',
92+
],
93+
'empty endpoint string',
94+
];
95+
96+
yield 'non-array metadata' => ['not an array', 'non-array metadata'];
97+
}
98+
}

0 commit comments

Comments
 (0)