From be7cf23efbb08043ba8bcc2ec0aa33774810f81f Mon Sep 17 00:00:00 2001 From: Isaac Bowen <84106134+isaac-bowen@users.noreply.github.com> Date: Mon, 10 Nov 2025 12:10:24 -0700 Subject: [PATCH] fix: allow multi-segment issuer paths --- src/Server/Registrar.php | 4 +- tests/Unit/Server/RegistrarTest.php | 93 +++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/Server/Registrar.php b/src/Server/Registrar.php index 47fe3e7..bbf9fdd 100644 --- a/src/Server/Registrar.php +++ b/src/Server/Registrar.php @@ -89,7 +89,7 @@ public function oauthRoutes(string $oauthPrefix = 'oauth'): void 'resource' => url('/'.$path), 'authorization_servers' => [url('/'.$path)], 'scopes_supported' => ['mcp:use'], - ]))->name('mcp.oauth.protected-resource'); + ]))->where('path', '.*')->name('mcp.oauth.protected-resource'); Router::get('/.well-known/oauth-authorization-server/{path?}', fn (?string $path = '') => response()->json([ 'issuer' => url('/'.$path), @@ -100,7 +100,7 @@ public function oauthRoutes(string $oauthPrefix = 'oauth'): void 'code_challenge_methods_supported' => ['S256'], 'scopes_supported' => ['mcp:use'], 'grant_types_supported' => ['authorization_code', 'refresh_token'], - ]))->name('mcp.oauth.authorization-server'); + ]))->where('path', '.*')->name('mcp.oauth.authorization-server'); Router::post($oauthPrefix.'/register', OAuthRegisterController::class); } diff --git a/tests/Unit/Server/RegistrarTest.php b/tests/Unit/Server/RegistrarTest.php index be12b7c..ad9aaf3 100644 --- a/tests/Unit/Server/RegistrarTest.php +++ b/tests/Unit/Server/RegistrarTest.php @@ -269,3 +269,96 @@ public function createAuthorizationCodeGrantClient(string $name, array $redirect $response->assertStatus(422); }); + +it('handles oauth discovery with multi-segment paths', function (): void { + // Fake Passport's routes so the oauthRoutes() helper can resolve them. + Route::get('/oauth/authorize')->name('passport.authorizations.authorize'); + Route::post('/oauth/token')->name('passport.token'); + + $registrar = new Registrar; + $registrar->oauthRoutes(); + + // Test protected resource endpoint with multi-segment path + $response = $this->getJson('/.well-known/oauth-protected-resource/mcp/weather'); + + $response->assertStatus(200); + $response->assertJson([ + 'resource' => url('/mcp/weather'), + 'authorization_servers' => [url('/mcp/weather')], + 'scopes_supported' => ['mcp:use'], + ]); + + // Test authorization server endpoint with multi-segment path + $response = $this->getJson('/.well-known/oauth-authorization-server/mcp/weather'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'issuer', + 'authorization_endpoint', + 'token_endpoint', + 'registration_endpoint', + 'response_types_supported', + 'code_challenge_methods_supported', + 'scopes_supported', + 'grant_types_supported', + ]); + $response->assertJson([ + 'issuer' => url('/mcp/weather'), + 'scopes_supported' => ['mcp:use'], + 'response_types_supported' => ['code'], + 'code_challenge_methods_supported' => ['S256'], + 'grant_types_supported' => ['authorization_code', 'refresh_token'], + ]); +}); + +it('handles oauth discovery with single segment paths', function (): void { + // Fake Passport's routes so the oauthRoutes() helper can resolve them. + Route::get('/oauth/authorize')->name('passport.authorizations.authorize'); + Route::post('/oauth/token')->name('passport.token'); + + $registrar = new Registrar; + $registrar->oauthRoutes(); + + // Test backward compatibility with single-segment paths + $response = $this->getJson('/.well-known/oauth-protected-resource/mcp'); + + $response->assertStatus(200); + $response->assertJson([ + 'resource' => url('/mcp'), + 'authorization_servers' => [url('/mcp')], + 'scopes_supported' => ['mcp:use'], + ]); + + $response = $this->getJson('/.well-known/oauth-authorization-server/mcp'); + + $response->assertStatus(200); + $response->assertJson([ + 'issuer' => url('/mcp'), + ]); +}); + +it('handles oauth discovery with no path', function (): void { + // Fake Passport's routes so the oauthRoutes() helper can resolve them. + Route::get('/oauth/authorize')->name('passport.authorizations.authorize'); + Route::post('/oauth/token')->name('passport.token'); + + $registrar = new Registrar; + $registrar->oauthRoutes(); + + // Test with no path (root) + $response = $this->getJson('/.well-known/oauth-protected-resource'); + + $response->assertStatus(200); + $response->assertJson([ + 'resource' => url('/'), + 'authorization_servers' => [url('/')], + 'scopes_supported' => ['mcp:use'], + ]); + + $response = $this->getJson('/.well-known/oauth-authorization-server'); + + $response->assertStatus(200); + $response->assertJson([ + 'issuer' => url('/'), + ]); +});