diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 14f4c6198..b465b2528 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -2,7 +2,12 @@ namespace App\Exceptions; +use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; +use Illuminate\Http\Response; use Sentry\Laravel\Integration; use Throwable; @@ -25,4 +30,32 @@ public function register(): void Integration::captureUnhandledException($e); }); } + + /** + * Convert an authentication exception into a response. + * + * Passport's AuthorizationController throws AuthenticationException for guests. + * Laravel's default falls back to route('login'), which this app does not define + * at /login (customer login is customer.login; admin is filament.admin.auth.login). + * + * @param Request $request + * @return Response|JsonResponse|RedirectResponse + */ + protected function unauthenticated($request, AuthenticationException $exception) + { + if ($this->shouldReturnJson($request, $exception)) { + return response()->json(['message' => $exception->getMessage()], 401); + } + + if ($this->isOAuthAuthenticationRequest($request)) { + return redirect()->guest(route('filament.admin.auth.login')); + } + + return redirect()->guest($exception->redirectTo($request) ?? route('customer.login')); + } + + private function isOAuthAuthenticationRequest(Request $request): bool + { + return $request->is('oauth/*') || $request->routeIs('passport.*'); + } } diff --git a/routes/ai.php b/routes/ai.php index 042791b88..408f02a01 100644 --- a/routes/ai.php +++ b/routes/ai.php @@ -24,6 +24,8 @@ ->name('mcp.oauth.admin.protected-resource'); Route::get('/.well-known/oauth-authorization-server', [McpOAuthController::class, 'authorizationServer']); +Route::redirect('/oauth/login', '/admin/login')->name('login'); + Route::prefix('oauth')->group(function (): void { Route::post('/register', RegisterClientController::class)->middleware('throttle:20,1'); Route::post('/token', [AccessTokenController::class, 'issueToken']) diff --git a/tests/Feature/Mcp/AdminMcpOAuthTest.php b/tests/Feature/Mcp/AdminMcpOAuthTest.php index 09f63578b..be93de506 100644 --- a/tests/Feature/Mcp/AdminMcpOAuthTest.php +++ b/tests/Feature/Mcp/AdminMcpOAuthTest.php @@ -89,6 +89,23 @@ public function test_admin_can_connect_via_oauth_and_use_admin_mcp_server(): voi $this->assertContains('admin-search-support-tickets', $names); } + public function test_guest_authorize_redirects_to_admin_login_instead_of_500(): void + { + $client = $this->createMcpOAuthClient(); + $parameters = $this->mcpOAuthAuthorizationParameters( + $client, + str_repeat('a', 64), + 'mcp:admin', + $this->mcpAdminOAuthResource(), + ); + + $this->get('/oauth/authorize?'.http_build_query($parameters)) + ->assertRedirect(route('filament.admin.auth.login')); + + $this->assertSame(url('/oauth/login'), route('login')); + $this->get(route('login'))->assertRedirect('/admin/login'); + } + public function test_non_admins_cannot_approve_admin_mcp_scope(): void { $user = User::factory()->create();