Laravel MCP OAuth认证服务

为Laravel MCP服务器提供OAuth 2.0认证功能,支持PKCE、UUID用户标识和跨域资源共享(CORS),适用于需要安全API访问的应用场景。

  • by codewarrior4
  • Blade
  • 本地部署
  • 1 星标
  • 更新于
在 GitHub 查看 codewarrior4/laravel-mcp-integration-with-passport

使用说明

Laravel MCP 配备 Passport OAuth 的设置指南

本指南详细记录了在使用Laravel Passport的Laravel MCP(模型上下文协议)服务器上实施OAuth认证的完整设置过程。

✅ 工作设置(或“运行环境”)

此项目已全面配置并经过测试,使用了:

  • 支持PKCE的OAuth 2.0认证
  • 用户使用的UUID主键
  • 基于会话的网页认证
  • 受保护的和公开的MCP端点
  • MCP Inspector 集成

快速入门

如果这已经设置好了,只需运行:

# Start the server
php artisan serve

# In another terminal, test with MCP Inspector
php artisan mcp:inspector mcp/admin

OAuth 凭据:

  • 在你的数据库中查找客户端ID和密钥 oauth_clients 桌子
  • 测试用户: test@example.com / password

先决条件

  • Laravel 11.x
  • PHP 8.1及以上版本
  • MySQL 8.0及以上版本
  • Composer(注:Composer在编程领域通常指一个依赖管理工具,用于PHP等语言的项目中,此处直接保留原词,不作具体翻译)

步骤1:安装Laravel Passport

php artisan install:api --passport

这个命令将:

  • 安装 Laravel Passport 包
  • 发布并运行Passport迁移
  • 为安全访问令牌生成加密密钥

步骤2:为UUID配置用户模型

更新 app/Models/User.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable implements OAuthenticatable
{
    use HasApiTokens, Notifiable, HasUuids;
    
    // ... rest of your model
}

重要的这个 HasUuids 如果您的用户表使用UUID作为主键,则需要此特性。

步骤3:配置身份验证守护程序

更新 config/auth.php:

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    'api' => [
        'driver' => 'passport',  // Changed from 'token' to 'passport'
        'provider' => 'users',
    ],
],

步骤4:修复UUID的护照迁移问题

关键的;严重的如果使用UUID,请更新Passport迁移以使用 foreignUuid 而不是 foreignId:

编辑这些迁移文件:

  • database/migrations/*_create_oauth_auth_codes_table.php
  • database/migrations/*_create_oauth_access_tokens_table.php
  • database/migrations/*_create_oauth_device_codes_table.php

变化:

$table->foreignId('user_id')->index();

收件人:

$table->foreignUuid('user_id')->index();

同时更新 database/migrations/*_create_users_table.php 会话表:

$table->foreignUuid('user_id')->nullable()->index();

然后运行迁移:

php artisan migrate:fresh

步骤5:配置CORS

跨域资源共享(CORS)对于OAuth流程至关重要,尤其是当MCP检查器或AI代理运行在不同的源时。

更新 config/cors.php:

<?php

return [
    'paths' => ['api/*', 'mcp/*', 'oauth/*', '.well-known/*'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],  // For production, specify exact origins

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,  // Required for OAuth with cookies
];

CORS 配置详解

  • 路径包含 oauth/* 对于OAuth端点以及 .well-known/* 用于OAuth发现
  • **允许的来源(或:允许的起源)**设置为 ['*'] 用于开发。在生产环境中,请指定确切的来源,如 ['https://your-domain.com']
  • 支持凭据必须是 true 在跨域请求中允许cookies和认证头信息
  • 允许的方法: ['*'] 允许所有HTTP方法(GET、POST、OPTIONS等)
  • 允许的头部(信息): ['*'] 允许所有头部信息,包括 Authorization 以及自定义头部

生产环境CORS配置

对于生产环境,请收紧CORS设置:

'allowed_origins' => [
    'https://your-production-domain.com',
    'https://mcp-inspector.example.com',
],

'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],

'supports_credentials' => true,

步骤5:配置护照授权视图

更新 app/Providers/AppServiceProvider.php:

use Laravel\Passport\Passport;

public function boot(): void
{
    Passport::authorizationView('mcp.authorize');
}

步骤6:发布MCP授权视图

php artisan vendor:publish --tag=mcp-views

这产生了 resources/views/mcp/authorize.blade.php。

步骤7:修复授权视图CSS加载问题

编辑 resources/views/mcp/authorize.blade.php 并替换为 @vite 使用内联样式指令以避免加载问题:

<style>
    body { font-family: sans-serif; margin: 0; padding: 0; }
    .bg-background { background: #f5f5f5; }
    .text-foreground { color: #333; }
    .bg-card { background: white; }
    .text-card-foreground { color: #333; }
    .border { border: 1px solid #e5e7eb; }
    .rounded-lg { border-radius: 0.5rem; }
    .shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); }
    .text-primary { color: #4f46e5; }
    .bg-primary { background: #4f46e5; }
    .text-primary-foreground { color: white; }
    .bg-muted\/50 { background: rgba(243, 244, 246, 0.5); }
    .text-muted-foreground { color: #6b7280; }
    button:hover { opacity: 0.9; }
</style>

步骤8:修复授权视图状态参数

编辑 resources/views/mcp/authorize.blade.php 并更新隐藏状态输入:

改变:

<input type="hidden" name="state" value="">

收件人:

<input type="hidden" name="state" value="{{ $request->state ?? '' }}">

这适用于批准和拒绝两种表单。

步骤9:配置MCP OAuth路由

更新 routes/ai.php:

<?php

use App\Mcp\Servers\AdminServer;
use App\Mcp\Servers\WarriorServer;
use Laravel\Mcp\Facades\Mcp;

// OAuth discovery and client registration routes
Mcp::oauthRoutes();

// Public MCP server (no auth)
Mcp::web('/mcp/warrior', WarriorServer::class);

// Admin MCP server (OAuth protected)
Mcp::web('/mcp/admin', AdminServer::class)
    ->middleware('auth:api');

步骤10:设置网页认证

在授权OAuth客户端之前,您需要一个网页认证系统供用户登录。

选项A:简单登录(用于测试)

创建 routes/auth.php:

<?php

use App\Http\Controllers\Auth\AuthenticatedSessionController;
use Illuminate\Support\Facades\Route;

Route::middleware('guest')->group(function () {
    Route::get('login', [AuthenticatedSessionController::class, 'create'])->name('login');
    Route::post('login', [AuthenticatedSessionController::class, 'store']);
});

Route::middleware('auth')->group(function () {
    Route::get('logout', [AuthenticatedSessionController::class, 'destroy'])->name('logout');
});

创造 app/Http/Controllers/Auth/AuthenticatedSessionController.php:

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;

class AuthenticatedSessionController extends Controller
{
    public function create(Request $request): View
    {
        return view('auth.login');
    }

    public function store(LoginRequest $request): RedirectResponse
    {
        $request->authenticate();
        $request->session()->regenerate();
        return redirect()->intended(route('dashboard'));
    }

    public function destroy(Request $request): RedirectResponse
    {
        Auth::guard('web')->logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();
        return redirect('/');
    }
}

创建 app/Http/Requests/Auth/LoginRequest.php:

<?php

namespace App\Http\Requests\Auth;

use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

class LoginRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'email' => ['required', 'string', 'email'],
            'password' => ['required', 'string'],
        ];
    }

    public function authenticate(): void
    {
        $this->ensureIsNotRateLimited();

        if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
            RateLimiter::hit($this->throttleKey());
            throw ValidationException::withMessages([
                'email' => trans('auth.failed'),
            ]);
        }

        RateLimiter::clear($this->throttleKey());
    }

    public function ensureIsNotRateLimited(): void
    {
        if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
            return;
        }

        event(new Lockout($this));
        $seconds = RateLimiter::availableIn($this->throttleKey());

        throw ValidationException::withMessages([
            'email' => trans('auth.throttle', [
                'seconds' => $seconds,
                'minutes' => ceil($seconds / 60),
            ]),
        ]);
    }

    public function throttleKey(): string
    {
        return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
    }
}

创建 resources/views/auth/login.blade.php:

<!DOCTYPE html>
<html>
<head>
    <title>Login - {{ config('app.name') }}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body style="display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:sans-serif;background:#f5f5f5;margin:0;">
    <div style="background:white;padding:40px;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.1);max-width:400px;width:100%;">
        <h2 style="margin:0 0 10px;text-align:center;">MCP OAuth Login</h2>
        <p style="color:#666;text-align:center;margin:0 0 30px;">Login to authorize the application</p>
        
        @if($errors->any())
            <div style="background:#fee;color:#c33;padding:10px;border-radius:4px;margin-bottom:20px;">
                {{ $errors->first() }}
            </div>
        @endif
        
        <form method="POST" action="/login">
            @csrf
            
            <div style="margin-bottom:20px;">
                <label style="display:block;margin-bottom:5px;font-weight:500;">Email</label>
                <input type="email" name="email" value="{{ old('email') }}" required autofocus
                    style="width:100%;padding:10px;border:1px solid #ddd;border-radius:4px;font-size:14px;">
            </div>
            
            <div style="margin-bottom:20px;">
                <label style="display:block;margin-bottom:5px;font-weight:500;">Password</label>
                <input type="password" name="password" required
                    style="width:100%;padding:10px;border:1px solid #ddd;border-radius:4px;font-size:14px;">
            </div>
            
            <button type="submit" style="width:100%;padding:12px;font-size:16px;cursor:pointer;background:#4f46e5;color:white;border:none;border-radius:6px;font-weight:500;">
                Login
            </button>
        </form>
    </div>
</body>
</html>

选项B:使用Laravel Breeze(推荐用于生产环境)

composer require laravel/breeze --dev
php artisan breeze:install blade
php artisan migrate
npm install && npm run build

步骤10:更新网页路由

更新 routes/web.php:

<?php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::middleware(['auth'])->group(function () {
    Route::get('/dashboard', function () {
        return 'Dashboard - You are logged in as ' . auth()->user()->email;
    })->name('dashboard');
});

require __DIR__.'/auth.php';

注没有从...重定向 /authorize 到;向;朝;对于;为了 /oauth/authorize 是需要的。该 Mcp::oauthRoutes('oauth') 调用时会自动注册Passport路由于 /oauth/authorize。

步骤11:创建OAuth客户端

php artisan passport:client

当被提示时:

  • 输入客户端名称(例如,“MCP 管理员检查员”)
  • 将重定向URI留空(按回车键)

保存生成的客户端ID和客户端密钥。

步骤12:更新客户端重定向URI

重定向URI需要存储为JSON数组。请手动更新它:

mysql -u root your_database_name
UPDATE oauth_clients 
SET redirect_uris = '["http://localhost:6274/oauth/callback"]' 
WHERE id = 'your-client-id';

或者使用tinker:

php artisan tinker
$client = \Laravel\Passport\Client::find('your-client-id');
$client->redirect = ['http://localhost:6274/oauth/callback'];
$client->save();

步骤13:创建测试用户

php artisan tinker
\App\Models\User::create([
    'name' => 'Test User',
    'email' => 'test@example.com',
    'password' => bcrypt('password')
]);

步骤14:测试OAuth流程

  1. 启动MCP检查器:
php artisan mcp:inspector mcp/admin
  1. 在检查员仪表盘中:

    • 请输入您的客户端ID
    • 输入您的客户端密钥
    • 点击“连接”
  2. 浏览器将打开到授权页面:

    • 使用您的测试用户凭据登录
    • 点击“授权”
    • 浏览器重定向回检查器
    • 检查员现在可以访问受保护的MCP服务器

OAuth流程图

┌─────────────┐                                    ┌──────────────┐
│             │  1. Request /mcp/admin             │              │
│ MCP         │───────────────────────────────────>│   Laravel    │
│ Inspector   │                                    │   MCP App    │
│             │  2. 401 Unauthenticated            │              │
│             │<───────────────────────────────────│              │
└─────────────┘                                    └──────────────┘
       │                                                   │
       │ 3. Redirect to /oauth/authorize                  │
       │──────────────────────────────────────────────────>
       │                                                   │
       │ 4. Not logged in → redirect to /login            │
       │<──────────────────────────────────────────────────
       │                                                   │
       │ 5. User enters credentials                       │
       │──────────────────────────────────────────────────>
       │                                                   │
       │ 6. Login successful → redirect to /oauth/authorize
       │<──────────────────────────────────────────────────
       │                                                   │
       │ 7. Show authorization screen                     │
       │<──────────────────────────────────────────────────
       │                                                   │
       │ 8. User clicks "Authorize"                       │
       │──────────────────────────────────────────────────>
       │                                                   │
       │ 9. Redirect with authorization code              │
       │<──────────────────────────────────────────────────
       │                                                   │
       │ 10. Exchange code for access token               │
       │──────────────────────────────────────────────────>
       │                                                   │
       │ 11. Return access token                          │
       │<──────────────────────────────────────────────────
       │                                                   │
       │ 12. Access /mcp/admin with token                 │
       │──────────────────────────────────────────────────>
       │                                                   │
       │ 13. Return MCP server response                   │
       │<──────────────────────────────────────────────────

故障排除

问题:“invalid_client”错误

解决方案检查以下内容:

  • 客户端ID存在于 oauth_clients 桌子
  • redirect_uris 以JSON数组形式存储: ["http://localhost:6274/oauth/callback"]
  • 客户端未被撤销
  • 你在使用 纯文本 客户端密钥(非数据库中的哈希版本)

问题:“invalid_request”(无效请求)- 请检查 client_secret 参数

解决方案:

  • 确保您使用的是创建客户端时显示的原始客户端密钥
  • 数据库中存储的是哈希版本——你需要原始的明文秘密
  • 如果丢失,请创建一个新的客户端: php artisan passport:client --no-interaction

问题:登录循环 - 成功认证后重定向回登录页面

解决方案:

  • 最常见的会话表 user_id 列类型不匹配
    • 如果使用UUID:更改 foreignId('user_id') to foreignUuid('user_id') 在会话表迁移过程中
    • 跑 php artisan migrate:fresh
  • 清除会话: mysql -u root your_db -e "TRUNCATE TABLE sessions;"
  • 清除缓存: php artisan optimize:clear
  • 确保 SESSION_DRIVER=database 在里面 .env

问题:“在OAuth授权过程中,请求缺少必需的参数”

解决方案:

  • 检查一下是否 state 参数正在通过授权表单传递
  • 更新 resources/views/mcp/authorize.blade.php:
    <input type="hidden" name="state" value="{{ $request->state ?? '' }}">
    

问题:UUID 兼容性错误 / “无效的用户ID”

解决方案:

  • 关键的;严重的;危急的所有引用(的外键) users.id 必须使用 foreignUuid() 如果用户表使用UUIDs
  • 更新这些迁移:
    • *_create_oauth_auth_codes_table.php
    • *_create_oauth_access_tokens_table.php
    • *_create_oauth_device_codes_table.php
    • *_create_users_table.php (会话表)
    • *_create_transactions_table.php (如果存在)
  • 添加 HasUuids “trait to User model” 可以翻译为“应用于用户模型的特性”或“用户模型的特性/特质”。这里,“trait”指的是某个特定的特征或属性,“User model”指的是用户模型。根据上下文,具体翻译可能略有不同,但基本意思是指将某个特性或属性应用到或描述用户模型
  • 跑 php artisan migrate:fresh

问题:授权时出现“无法获取”错误

解决方案:

  • 移除 @vite 来自授权视图的指令
  • 使用内联CSS代替
  • 如有需要,请在授权视图中禁用JavaScript

问题:浏览器控制台中的CORS错误

解决方案:

  • 验证 config/cors.php 包括 oauth/* 在路径中
  • 确保 supports_credentials 被设定为 true
  • 检查一下 allowed_origins 包括请求的来源
  • 清除配置缓存: php artisan config:clear

问题:预检OPTIONS请求失败

解决方案:

  • 确保CORS中间件已注册在 bootstrap/app.php
  • 验证 allowed_methods 包括 OPTIONS
  • 检查Web服务器(nginx/Apache)是否阻止了OPTIONS请求

安全考虑事项

  1. 永远不要提交OAuth密钥添加到 .gitignore:

    storage/oauth-*.key
    
  2. 在生产环境中使用HTTPS更新 .env:

    APP_URL=https://your-domain.com
    
  3. 在生产环境中限制CORS来源更新 config/cors.php:

    'allowed_origins' => ['https://your-domain.com'],
    
  4. 定期更换密钥定期生成新的OAuth客户端

  5. 实现作用域为不同的访问级别定义特定权限

  6. 监控OAuth使用情况记录授权尝试和令牌使用情况

  7. 限制OAuth端点的速率防止暴力破解攻击

额外资源

摘要

现在,您已经拥有了一个功能齐全的Laravel MCP服务器,配置了OAuth认证和CORS:

  • ✅ 公共MCP服务器位于 /mcp/warrior (无需认证)
  • ✅ 已保护MCP服务器于 /mcp/admin (需要OAuth)
  • ✅ 带有用户登录的OAuth授权流程
  • ✅ 已为跨域请求配置了CORS(跨域资源共享)
  • ✅ 基于令牌的安全API访问

现在,AI代理可以从任何来源使用标准的OAuth 2.0流程对您的受保护MCP服务器进行身份验证和访问。