MCP認証・認可
LaseのMCP経路は、個人利用にOAuth 2.1 Authorization Code + PKCE、無人連携にテナントService Tokenを使用する。旧Sanctum PAT方式はMCPから削除した。Sanctumは通常のWeb/APIセッション用途では引き続き利用できる。
本番要件と環境変数
- HTTPSを必須にする(
localhostと*.localhostだけは開発用HTTPを許容)。 - PHPの
ext-sodiumを導入する。 - Passport署名鍵は環境ごとに一度だけ生成し、Secret Managerから
PASSPORT_PRIVATE_KEYとPASSPORT_PUBLIC_KEYへ注入する。デプロイごとに再生成してはいけない。 - Proxy配下ではLaravelのtrusted proxyを正しく設定し、外部URLがHTTPSで生成されるようにする。
MCPをまだ提供しないアプリは、LASE_MCP_ENABLED=falseを設定する。このときOAuth Discovery・Token・認可・動的クライアント登録と、テナントのService Token・OAuth同意APIは登録されない。TenantMcpRegistrarによるMCP endpointの登録も例外で拒否される。
| 変数 | 既定値 | 用途 |
|---|---|---|
LASE_MCP_ENABLED | true | MCPのルート・API・endpoint登録を有効化する。未提供のアプリではfalseを設定する |
PASSPORT_PRIVATE_KEY | なし | OAuth Access Tokenを署名するPassport秘密鍵。Secret Managerから注入する |
PASSPORT_PUBLIC_KEY | なし | OAuth Access Tokenの署名を検証するPassport公開鍵。Secret Managerから注入する |
LASE_MCP_OAUTH_ISSUER | APP_URL | Authorization Serverのissuer。外部から到達できる固定URL |
LASE_MCP_RESOURCE_URI_TEMPLATE | https://{tenant}.APP_DOMAIN/mcp | tenantごとの正規resource URI |
LASE_MCP_ACCESS_TOKEN_TTL_MINUTES | 60 | Access Token TTL |
LASE_MCP_REFRESH_TOKEN_TTL_DAYS | 30 | Refresh Token TTL |
LASE_MCP_SERVICE_TOKEN_TTL_DAYS | 90 | Service Token既定TTL |
LASE_MCP_SERVICE_TOKEN_MAX_TTL_DAYS | 365 | Service Token最大TTL |
LASE_MCP_REQUIRE_ACTIVE_SUBSCRIPTION | true | 現在の契約期間外のtenantを拒否 |
LASE_MCP_RATE_LIMIT_PER_MINUTE | 60 | tenant/principal/IP単位の上限 |
LASE_MCP_ALLOWED_ORIGINS | 空 | 同一Origin以外に追加で許可するOrigin |
php artisan migrateはOAuth、consent、Service Token、監査ログ用テーブルを作成し、tenants.mcp_enabledを既定値trueで追加する。無効tenant、削除tenant、契約期間外tenantは各リクエストで拒否する。契約の判定が製品固有の場合はMcpTenantEligibilityをアプリ側で再bindする。
mcp_enabledは運営管理画面のテナント編集から切り替える。無効にすると、そのtenantへのMCP接続は発行済みService Token・OAuth Tokenを含めてすべて拒否される。
Passport 13は上記の標準環境変数を直接参照する。通常のデプロイ処理ではphp artisan passport:keysを実行しない。鍵の変更は既存OAuth Tokenを無効化するため、初回構築または明示的な鍵ローテーション手順としてのみ実施する。Service Token認証はPassport鍵を使用せず、OAuth Bearer Tokenを検証するときだけPassport公開鍵を解決する。
$this->app->bind(
\CodebaseJp\Lase\Contracts\McpTenantEligibility::class,
\App\Mcp\FlowtTenantEligibility::class,
);Service Tokenは発行ユーザーから独立したサービス主体である。発行者が退職・停止しても自動失効せず、明示的な失効またはtenantの利用停止で止める。発行者IDは追跡用に保持する。
Discovery
MCPクライアントは未認証アクセスの401とWWW-Authenticateのresource_metadataから次を発見する。
/.well-known/oauth-protected-resource{/resource/path}: RFC 9728 Protected Resource Metadata/.well-known/oauth-authorization-server: RFC 8414 Authorization Server Metadata/oauth/authorize: 認可画面/oauth/token: token endpoint
認可・tokenリクエストの両方に同じRFC 8707 resourceを渡す。Access Tokenは単一resource/tenantに固定され、別ホスト・別パス・別tenantでは拒否される。OIDCログインを提供していないためOIDC Discoveryは公開しない。OAuth Client ID Metadata Documentsのリモート取得とDynamic Client Registrationは、SSRFや無制限登録を避けるため未対応でありmetadataにもadvertiseしない。
OAuth Client登録と個人認可
動的クライアント登録(既定)
MCPクライアントはregistration_endpoint({issuer}/oauth/register)へRFC 7591の登録要求を送ることで、事前登録なしに接続できる。Discoveryのメタデータにregistration_endpointを広告するため、対応クライアントは自動で登録する。
curl -X POST https://flowt.example.com/oauth/register \
-H 'Content-Type: application/json' \
-d '{"client_name":"Claude Code","redirect_uris":["http://localhost:33418/callback"]}'{
"client_id": "9f1c...",
"client_id_issued_at": 1788000000,
"client_name": "Claude Code",
"redirect_uris": ["http://localhost:33418/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}token_endpoint_auth_methodは認可サーバーのメタデータが広告するnone・client_secret_basic・client_secret_postを受け付ける。none以外を指定した場合はconfidential clientとして登録し、client_secretとclient_secret_expires_at(無期限を表す0)を返す。平文のsecretはこの応答でしか取得できない。ネイティブクライアントはPKCEを使うpublic client(none)を選ぶ。
受け入れるRedirect URIは次のとおり。1回の登録で最大5個まで、grant_typesはauthorization_codeとrefresh_token、response_typesはcodeを受け付ける。
| スキーム | 可否 |
|---|---|
https | 許可 |
http | ループバック(localhost・127.0.0.1・::1・*.localhost)のみ許可 |
| 独自スキーム(RFC 8252 §7.1のprivate-use URI scheme) | 許可。逆引きDNS形式(com.example.app:/oauth2redirect/cb)を推奨 |
javascript・data・file・about・blob・ws/wssなど | 拒否 |
いずれの場合もfragment・userinfo・ホストのwildcardは拒否する。
登録しただけではテナントのデータへ到達できない。実際の利用にはユーザーのログインと、テナント・Scopeを選ぶ同意が必要であり、同意は組織管理の「MCP連携」画面から解除できる。誰でも登録できる経路のため、既定で1時間あたり10回のレート制限をかけている。client_nameは登録側が自由に設定できるため、同意画面には認可後の転送先(redirect_uri)も表示し、ユーザーが名前だけで判断しないようにしている。
自動登録を止める場合はLASE_MCP_DYNAMIC_REGISTRATION_ENABLED=falseを設定する。無効にするとエンドポイントを登録せず、メタデータにもregistration_endpointを広告しない。
事前登録(動的登録を無効にする場合)
php artisan passport:client --public --name="固定済みのMCP Client名" \
--redirect_uri="http://localhost:PORT/callback"このコマンドはRedirect URI確定後の初回構築時に一度だけ、管理された運用作業として実行し、発行されたClient IDをクライアント設定へ安全に反映する。Redirect URIはクライアントが送る値と完全一致させる。ネイティブクライアントの多くはhttp://localhost:{port}/callbackを使うため、127.0.0.1で登録すると一致しない点に注意する。
利用中のPassportバージョンで--publicがない場合は対話式コマンドでpublic clientを選ぶ。wildcard redirect URIは禁止する。Clientのgrant_typesはauthorization_codeとrefresh_tokenだけにする。固定Clientの冪等初期登録を将来実装する場合も、Client IDを安定させ、登録済みRedirect URIとの完全一致を確認する独立した初期構築処理とし、通常デプロイからは分離する。
認可リクエストにはresponse_type=code、登録済みclient_id/redirect_uri、推測困難なstate、code_challenge_method=S256、PKCE challenge、scope、絶対resourceが必要である。Laseはstateの存在を必須化してそのまま返す。state値の一致検証はOAuth仕様どおりclient側の責務である。認可画面はログイン済みユーザーのACTIVE membershipだけを表示し、CSRF保護されたPOSTでtenantを確定する。コードはPassportにより短時間・単回利用となり、refresh tokenは利用のたびにrotate/revokeされる。
ユーザーはGET /api/tenant/mcp-oauth-consentsでClient別同意を確認し、DELETE /api/tenant/mcp-oauth-consents/{id}でClient単位に同意、Access Token、Refresh Tokenを一括失効できる。
ScopeとTool
アプリはconfig/lase.phpのmcp.scopesへscopeを追加する。Service Tokenの初期選択は読み取りscopeを基本とし、不要なwrite scopeを付与しない。
'mcp' => [
'scopes' => [
'workflows:read' => ['description' => 'ワークフローの参照', 'default' => true],
'workflows:start' => ['description' => 'ワークフローの開始', 'default' => false],
],
],ToolはTenantToolを継承し、RequiresScopesでAND (all) とOR (any) を宣言する。handleTenantには認証方式に依存しないMcpPrincipalContextが渡る。
use CodebaseJp\Lase\Contexts\McpPrincipalContext;
use CodebaseJp\Lase\Mcp\Attributes\RequiresScopes;
use CodebaseJp\Lase\Mcp\Tools\TenantTool;
#[RequiresScopes(all: ['workflows:read'], any: ['tasks:read', 'tasks:write'])]
final class ListWorkflows extends TenantTool
{
protected function handleTenant(Request $request, Tenant $tenant, McpPrincipalContext $principal): Response
{
return Response::structured(['workflows' => Workflow::where('tenant_id', $principal->tenantId)->get()]);
}
}個人OAuthだけに既存tenant permissionも要求する場合はrequiredPermission()をoverrideする。Service Tokenは人のpermissionを継承しないため、この種のToolへはscopeだけでアクセスさせずエラーになる。
MCP endpoint登録
Route::domain('{tenantCode}.'.config('app.domain'))->group(function () {
app(\CodebaseJp\Lase\Mcp\TenantMcpRegistrar::class)
->web('/mcp', \App\Mcp\Servers\FlowtServer::class);
});順序はchallenge付与、Origin検証、tenant特定、OAuth/Service Token認証、tenant・user・membership・resource検証、監査、rate limit、Tool実行である。challenge付与はlaravel/mcpがresource_metadataなしで上書きするWWW-Authenticateを最も外側で設定し直し、401応答から必ずProtected Resource Metadataへ辿れるようにする。McpPrincipalContextにはtype (user/service_token)、ID、tenant、scopes、該当するuser/client/token IDが入る。
Toolが監査へ業務属性を残す場合は、秘密情報や個人情報を除いた値だけを明示する。文字列は512文字に制限され、未加工の引数は自動記録されない。
app(\CodebaseJp\Lase\Mcp\McpAuditAttributes::class)->add('workflow_id', $workflow->id);Service Token運用
tenant管理画面へMcpServiceTokenIndexを組み込むか、以下のAPIを利用する。操作にはtenant permission mcp.service-tokens:manageが必要である。利用アプリ独自のpermission enumにも同じ値を追加する。
GET /api/tenant/mcp-service-tokensPOST /api/tenant/mcp-service-tokensPOST /api/tenant/mcp-service-tokens/{id}/rotateDELETE /api/tenant/mcp-service-tokens/{id}
import { useMcpServiceTokenApi } from '@codebase-jp/lase/api/tenant';
import { McpServiceTokenIndex } from '@codebase-jp/lase/views/tenant/mcp-service-tokens';平文は発行/rotationレスポンスで一度だけ返り、DBには高エントロピーtokenのSHA-256だけを保存する。token単位でscope、期限、IP/CIDR allowlist、最終利用日時、失効日時を持つ。連携先ごとに個別発行する。
通常のrotationは新tokenを安全な保管先へ設定して疎通後に旧tokenを失効する。APIのrotate操作は旧tokenを即時失効するため、停止時間を避ける場合は新規発行→切替→旧token失効の順に行う。漏洩時は対象tokenを即時DELETEし、OAuthならconsentを取り消し、mcp_audit_logsでprincipal、Tool、IP、request IDを確認する。監査ログはAuthorization header、token、未加工Tool引数を保存しない。
Flowt移行
- 旧
LASE_MCP_GUARD、LASE_MCP_ABILITY、mcp:usePAT発行処理を削除する。 - Flowt scope (
workflows:read等)をregistryへ追加し、各ToolへRequiresScopesを付ける。 handleTenantの第3引数をUserからMcpPrincipalContextへ変更する。- MCP endpointの正規URIを
LASE_MCP_RESOURCE_URI_TEMPLATEと一致させる。 - 環境ごとに一度生成したPassport鍵をSecret Managerから
PASSPORT_PRIVATE_KEYとPASSPORT_PUBLIC_KEYへ注入する。 - Redirect URI確定後、OAuth Clientを完全一致で一度だけ事前登録する。未確定の場合は登録せず、将来の管理画面またはDynamic Client Registrationを計画する。
- migration、署名鍵、Discovery、OAuth、Service Token、tenant分離、失効をstagingで確認する。