harden(mcp): enforce OAuth read-only server-side, not via scope

Block write tools whenever the request authenticated through the api
(Passport) guard, independent of the scopes a client requested. Previously
read-only relied on clients asking only for mcp:use and on tokenCan(); since
Passport always honours the '*' wildcard scope, a client that requested '*'
could have slipped past. Clamping on the resolving guard makes the locked
'OAuth = read-only' guarantee hold on the server.
This commit is contained in:
Víctor Falcón 2026-07-17 18:26:05 +02:00
parent 15d70d2db9
commit 5fa59e61d3
1 changed files with 11 additions and 0 deletions

View File

@ -10,6 +10,7 @@ use App\Models\Space;
use App\Models\Transaction;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
@ -27,6 +28,16 @@ abstract class WriteTool extends McpTool
{
protected function respond(Request $request, User $user): Response
{
// OAuth connections (Claude Desktop / ChatGPT) authenticate through the
// `api` (Passport) guard and are read-only in this iteration, whatever
// scope a client requests: writes need a Claude Code read & write
// personal access token. Clamping on the resolving guard keeps the
// guarantee server-side instead of trusting the client to request a
// narrow scope (Passport always honours the `*` wildcard scope).
if (Auth::getDefaultDriver() === 'api') {
return Response::error('OAuth connections are read-only. Use a Claude Code read & write token to make changes.');
}
if (! $user->tokenCan('mcp:write')) {
return Response::error('This token is read-only. Create a read & write token to make changes.');
}