diff --git a/src/Server/Http/Controllers/OAuthRegisterController.php b/src/Server/Http/Controllers/OAuthRegisterController.php index 00bf28fd6..b5115b654 100644 --- a/src/Server/Http/Controllers/OAuthRegisterController.php +++ b/src/Server/Http/Controllers/OAuthRegisterController.php @@ -21,8 +21,8 @@ class OAuthRegisterController public function __invoke(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ - 'client_name' => ['nullable', 'string', 'min:1', 'max:255', 'required_without:name'], - 'name' => ['nullable', 'string', 'min:1', 'max:255', 'required_without:client_name'], + 'client_name' => ['nullable', 'string', 'min:1', 'max:255'], + 'name' => ['nullable', 'string', 'min:1', 'max:255'], 'redirect_uris' => ['required', 'array', 'min:1'], 'redirect_uris.*' => ['required', 'string', function (string $attribute, $value, $fail): void { if (! $this->isValidRedirectUri($value)) { @@ -76,7 +76,7 @@ public function __invoke(Request $request): JsonResponse ); $client = $clients->createAuthorizationCodeGrantClient( - name: $validated['client_name'] ?? $validated['name'], + name: $this->resolveClientName($validated), redirectUris: $validated['redirect_uris'], confidential: false, enableDeviceFlow: false, @@ -92,6 +92,18 @@ public function __invoke(Request $request): JsonResponse ], 201); } + /** + * Resolve the client name, falling back to the redirect host or a default. + * + * @param array $validated + */ + protected function resolveClientName(array $validated): string + { + return $validated['client_name'] + ?? $validated['name'] + ?? (parse_url((string) ($validated['redirect_uris'][0] ?? ''), PHP_URL_HOST) ?: 'MCP Client'); + } + protected function isValidRedirectUri(string $value): bool { $scheme = parse_url($value, PHP_URL_SCHEME); diff --git a/tests/Unit/Server/RegistrarTest.php b/tests/Unit/Server/RegistrarTest.php index 666c83205..4919262e5 100644 --- a/tests/Unit/Server/RegistrarTest.php +++ b/tests/Unit/Server/RegistrarTest.php @@ -221,7 +221,7 @@ function ensureMockClientRepository(): void ]); }); -it('requires an oauth client name for registration', function (): void { +it('falls back to the redirect host when no client name is provided', function (): void { $clientRepository = new class { public ?string $capturedName = null; @@ -244,7 +244,25 @@ public function createAuthorizationCodeGrantClient(string $name, array $redirect $this->app->instance(ClientRepository::class, $clientRepository); $response = $this->postJson('/oauth/register', [ - 'redirect_uris' => ['http://localhost:3000/callback'], + 'redirect_uris' => ['https://example.com/callback'], + ]); + + $response->assertStatus(201); + + expect($clientRepository->capturedName)->toBe('example.com'); +}); + +it('returns invalid_client_metadata when client metadata is invalid', function (): void { + ensureMockClientRepository(); + + $registrar = new Registrar; + $registrar->oauthRoutes(); + + $this->app->instance(ClientRepository::class, new ClientRepository); + + $response = $this->postJson('/oauth/register', [ + 'client_name' => str_repeat('a', 256), + 'redirect_uris' => ['https://example.com/callback'], ]); $response->assertStatus(400); @@ -253,6 +271,38 @@ public function createAuthorizationCodeGrantClient(string $name, array $redirect ]); }); +it('preserves a falsy client name for oauth registration', function (): void { + $clientRepository = new class + { + public ?string $capturedName = null; + + public function createAuthorizationCodeGrantClient(string $name, array $redirectUris, bool $confidential = true, $user = null, bool $enableDeviceFlow = false) + { + $this->capturedName = $name; + + return (object) [ + 'id' => 'test-client-id', + 'grant_types' => ['authorization_code'], + 'redirect_uris' => $redirectUris, + ]; + } + }; + + $registrar = new Registrar; + $registrar->oauthRoutes(); + + $this->app->instance(ClientRepository::class, $clientRepository); + + $response = $this->postJson('/oauth/register', [ + 'client_name' => '0', + 'redirect_uris' => ['https://example.com/callback'], + ]); + + $response->assertStatus(201); + + expect($clientRepository->capturedName)->toBe('0'); +}); + it('falls back to the legacy name field for oauth registration', function (): void { $clientRepository = new class { @@ -699,7 +749,7 @@ public function createAuthorizationCodeGrantClient(string $name, array $redirect $this->app->instance(ClientRepository::class, new ClientRepository); $response = $this->post('/oauth/register', [ - 'redirect_uris' => ['http://localhost:3000/callback'], + 'redirect_uris' => ['not-a-valid-url'], ], ['Accept' => '*/*']); $response->assertStatus(400);