Skip to content

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_KEYPASSPORT_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_ENABLEDtrueMCPのルート・API・endpoint登録を有効化する。未提供のアプリではfalseを設定する
PASSPORT_PRIVATE_KEYなしOAuth Access Tokenを署名するPassport秘密鍵。Secret Managerから注入する
PASSPORT_PUBLIC_KEYなしOAuth Access Tokenの署名を検証するPassport公開鍵。Secret Managerから注入する
LASE_MCP_OAUTH_ISSUERAPP_URLAuthorization Serverのissuer。外部から到達できる固定URL
LASE_MCP_RESOURCE_URI_TEMPLATEhttps://{tenant}.APP_DOMAIN/mcptenantごとの正規resource URI
LASE_MCP_ACCESS_TOKEN_TTL_MINUTES60Access Token TTL
LASE_MCP_REFRESH_TOKEN_TTL_DAYS30Refresh Token TTL
LASE_MCP_SERVICE_TOKEN_TTL_DAYS90Service Token既定TTL
LASE_MCP_SERVICE_TOKEN_MAX_TTL_DAYS365Service Token最大TTL
LASE_MCP_REQUIRE_ACTIVE_SUBSCRIPTIONtrue現在の契約期間外のtenantを拒否
LASE_MCP_RATE_LIMIT_PER_MINUTE60tenant/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公開鍵を解決する。

php
$this->app->bind(
    \CodebaseJp\Lase\Contracts\McpTenantEligibility::class,
    \App\Mcp\FlowtTenantEligibility::class,
);

Service Tokenは発行ユーザーから独立したサービス主体である。発行者が退職・停止しても自動失効せず、明示的な失効またはtenantの利用停止で止める。発行者IDは追跡用に保持する。

Discovery

MCPクライアントは未認証アクセスの401WWW-Authenticateresource_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を広告するため、対応クライアントは自動で登録する。

bash
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"]}'
json
{
  "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は認可サーバーのメタデータが広告するnoneclient_secret_basicclient_secret_postを受け付ける。none以外を指定した場合はconfidential clientとして登録し、client_secretclient_secret_expires_at(無期限を表す0)を返す。平文のsecretはこの応答でしか取得できない。ネイティブクライアントはPKCEを使うpublic client(none)を選ぶ。

受け入れるRedirect URIは次のとおり。1回の登録で最大5個まで、grant_typesauthorization_coderefresh_tokenresponse_typescodeを受け付ける。

スキーム可否
https許可
httpループバック(localhost127.0.0.1::1*.localhost)のみ許可
独自スキーム(RFC 8252 §7.1のprivate-use URI scheme)許可。逆引きDNS形式(com.example.app:/oauth2redirect/cb)を推奨
javascriptdatafileaboutblobws/wssなど拒否

いずれの場合もfragment・userinfo・ホストのwildcardは拒否する。

登録しただけではテナントのデータへ到達できない。実際の利用にはユーザーのログインと、テナント・Scopeを選ぶ同意が必要であり、同意は組織管理の「MCP連携」画面から解除できる。誰でも登録できる経路のため、既定で1時間あたり10回のレート制限をかけている。client_nameは登録側が自由に設定できるため、同意画面には認可後の転送先(redirect_uri)も表示し、ユーザーが名前だけで判断しないようにしている。

自動登録を止める場合はLASE_MCP_DYNAMIC_REGISTRATION_ENABLED=falseを設定する。無効にするとエンドポイントを登録せず、メタデータにもregistration_endpointを広告しない。

事前登録(動的登録を無効にする場合)

bash
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_typesauthorization_coderefresh_tokenだけにする。固定Clientの冪等初期登録を将来実装する場合も、Client IDを安定させ、登録済みRedirect URIとの完全一致を確認する独立した初期構築処理とし、通常デプロイからは分離する。

認可リクエストにはresponse_type=code、登録済みclient_id/redirect_uri、推測困難なstatecode_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.phpmcp.scopesへscopeを追加する。Service Tokenの初期選択は読み取りscopeを基本とし、不要なwrite scopeを付与しない。

php
'mcp' => [
    'scopes' => [
        'workflows:read' => ['description' => 'ワークフローの参照', 'default' => true],
        'workflows:start' => ['description' => 'ワークフローの開始', 'default' => false],
    ],
],

ToolはTenantToolを継承し、RequiresScopesでAND (all) とOR (any) を宣言する。handleTenantには認証方式に依存しないMcpPrincipalContextが渡る。

php
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登録

php
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/mcpresource_metadataなしで上書きするWWW-Authenticateを最も外側で設定し直し、401応答から必ずProtected Resource Metadataへ辿れるようにする。McpPrincipalContextにはtype (user/service_token)、ID、tenant、scopes、該当するuser/client/token IDが入る。

Toolが監査へ業務属性を残す場合は、秘密情報や個人情報を除いた値だけを明示する。文字列は512文字に制限され、未加工の引数は自動記録されない。

php
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-tokens
  • POST /api/tenant/mcp-service-tokens
  • POST /api/tenant/mcp-service-tokens/{id}/rotate
  • DELETE /api/tenant/mcp-service-tokens/{id}
ts
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移行

  1. LASE_MCP_GUARDLASE_MCP_ABILITYmcp:use PAT発行処理を削除する。
  2. Flowt scope (workflows:read等)をregistryへ追加し、各ToolへRequiresScopesを付ける。
  3. handleTenantの第3引数をUserからMcpPrincipalContextへ変更する。
  4. MCP endpointの正規URIをLASE_MCP_RESOURCE_URI_TEMPLATEと一致させる。
  5. 環境ごとに一度生成したPassport鍵をSecret ManagerからPASSPORT_PRIVATE_KEYPASSPORT_PUBLIC_KEYへ注入する。
  6. Redirect URI確定後、OAuth Clientを完全一致で一度だけ事前登録する。未確定の場合は登録せず、将来の管理画面またはDynamic Client Registrationを計画する。
  7. migration、署名鍵、Discovery、OAuth、Service Token、tenant分離、失効をstagingで確認する。