Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.*');
}
}
2 changes: 2 additions & 0 deletions routes/ai.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down
17 changes: 17 additions & 0 deletions tests/Feature/Mcp/AdminMcpOAuthTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down