66
77use Mcp \Server \Authentication \Dto \OAuthMetadata ;
88use Mcp \Server \Authentication \Dto \OAuthProtectedResourceMetadata ;
9+ use Mcp \Server \Authentication \Router \AuthRouterOptions ;
910use Psr \Http \Message \ResponseFactoryInterface ;
1011use Psr \Http \Message \ResponseInterface ;
1112use Psr \Http \Message \ServerRequestInterface ;
@@ -23,6 +24,33 @@ public function __construct(
2324 private StreamFactoryInterface $ streamFactory ,
2425 ) {}
2526
27+ public static function create (
28+ AuthRouterOptions $ options ,
29+ ResponseFactoryInterface $ responseFactory ,
30+ StreamFactoryInterface $ streamFactory ,
31+ ): self {
32+ $ oauthMetadata = self ::createOAuthMetadata ($ options );
33+
34+ // Create protected resource metadata
35+ $ protectedResourceMetadata = new OAuthProtectedResourceMetadata (
36+ resource: $ options ->baseUrl ?? $ oauthMetadata ->getIssuer (),
37+ authorizationServers: [$ oauthMetadata ->getIssuer ()],
38+ jwksUri: null ,
39+ scopesSupported: empty ($ options ->scopesSupported ) ? null : $ options ->scopesSupported ,
40+ bearerMethodsSupported: null ,
41+ resourceSigningAlgValuesSupported: null ,
42+ resourceName: $ options ->resourceName ,
43+ resourceDocumentation: $ options ->serviceDocumentationUrl ,
44+ );
45+
46+ return new self (
47+ oauthMetadata: $ oauthMetadata ,
48+ protectedResourceMetadata: $ protectedResourceMetadata ,
49+ responseFactory: $ responseFactory ,
50+ streamFactory: $ streamFactory ,
51+ );
52+ }
53+
2654 public function handleOAuthMetadata (ServerRequestInterface $ request ): ResponseInterface
2755 {
2856 $ json = \json_encode ($ this ->oauthMetadata ->jsonSerialize (), JSON_THROW_ON_ERROR );
@@ -46,4 +74,58 @@ public function handleProtectedResourceMetadata(ServerRequestInterface $request)
4674 ->withHeader ('Cache-Control ' , 'public, max-age=3600 ' )
4775 ->withBody ($ body );
4876 }
77+
78+ private static function createOAuthMetadata (AuthRouterOptions $ options ): OAuthMetadata
79+ {
80+ self ::checkIssuerUrl ($ options ->issuerUrl );
81+
82+ $ baseUrl = $ options ->baseUrl ?? $ options ->issuerUrl ;
83+
84+ return new OAuthMetadata (
85+ issuer: $ options ->issuerUrl ,
86+ authorizationEndpoint: self ::buildUrl ('/oauth2/authorize ' , $ baseUrl ),
87+ tokenEndpoint: self ::buildUrl ('/oauth2/token ' , $ baseUrl ),
88+ responseTypesSupported: ['code ' ],
89+ registrationEndpoint: self ::buildUrl ('/oauth2/register ' , $ baseUrl ),
90+ scopesSupported: empty ($ options ->scopesSupported ) ? null : $ options ->scopesSupported ,
91+ responseModesSupported: null ,
92+ grantTypesSupported: ['authorization_code ' , 'refresh_token ' ],
93+ tokenEndpointAuthMethodsSupported: ['client_secret_post ' ],
94+ tokenEndpointAuthSigningAlgValuesSupported: null ,
95+ serviceDocumentation: $ options ->serviceDocumentationUrl ,
96+ revocationEndpoint: self ::buildUrl ('/oauth2/revoke ' , $ baseUrl ),
97+ revocationEndpointAuthMethodsSupported: ['client_secret_post ' ],
98+ revocationEndpointAuthSigningAlgValuesSupported: null ,
99+ introspectionEndpoint: null ,
100+ introspectionEndpointAuthMethodsSupported: null ,
101+ introspectionEndpointAuthSigningAlgValuesSupported: null ,
102+ codeChallengeMethodsSupported: ['S256 ' ],
103+ );
104+ }
105+
106+ private static function checkIssuerUrl (string $ issuer ): void
107+ {
108+ $ parsed = \parse_url ($ issuer );
109+
110+ // Technically RFC 8414 does not permit a localhost HTTPS exemption, but this is necessary for testing
111+ if (
112+ $ parsed ['scheme ' ] !== 'https ' &&
113+ !\in_array ($ parsed ['host ' ] ?? '' , ['localhost ' , '127.0.0.1 ' ], true )
114+ ) {
115+ throw new \InvalidArgumentException ('Issuer URL must be HTTPS ' );
116+ }
117+
118+ if (isset ($ parsed ['fragment ' ])) {
119+ throw new \InvalidArgumentException ("Issuer URL must not have a fragment: {$ issuer }" );
120+ }
121+
122+ if (isset ($ parsed ['query ' ])) {
123+ throw new \InvalidArgumentException ("Issuer URL must not have a query string: {$ issuer }" );
124+ }
125+ }
126+
127+ private static function buildUrl (string $ path , string $ baseUrl ): string
128+ {
129+ return \rtrim ($ baseUrl , '/ ' ) . $ path ;
130+ }
49131}
0 commit comments