Skip to content

feat(agent-commerce): エージェントコマース用 OAuth2(client_credentials / scope / AccessTokenHandler / クライアント登録導線)を追加 (#188) - #191

Merged
nanasess merged 4 commits into
EC-CUBE:4.4from
nanasess:feature/agentic-commerce
Aug 13, 2026

Conversation

@nanasess

@nanasess nanasess commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

概要

AI エージェント (ChatGPT / Gemini 等) → EC-CUBE のインバウンド machine-to-machine 認証を成立させるため、OAuth2 の client_credentials グラントを有効化し、エージェントコマース (ACP/UCP) 用の scope を登録します。あわせて、EC-CUBE 本体の AgentCommerceOAuth2Authenticator が依存する Symfony 標準 AccessTokenHandlerInterface の具象と、ACP/UCP 用クライアントの登録導線を提供します。

Closes #188

変更内容

認証基盤

  • client_credentials グラント有効化 (Resource/config/services.yaml)。
  • agentic scope 登録: <protocol>:<capability> 規約で 6 scope を scopes.available に追加 (acp:checkout / acp:catalog / ucp:checkout / ucp:cart / ucp:catalog / ucp:identity)。defaultread のまま (明示要求時のみ付与)。
  • Plugin\Api44\Security\AgentCommerceAccessTokenHandler (新規): league の ResourceServer で Bearer トークン (JWT) を検証 (公開鍵署名・有効期限・失効) し、付与 scope を Symfony の UserBadge attributes (scopes) に載せて返す。Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface を alias で束ね、本体の AgentCommerceOAuth2Authenticator@? optional 依存で解決する (api4 未導入時は本体が 503 を返す疎結合)。

管理画面: ACP/UCP クライアントの登録導線を分離

汎用の OAuth クライアント登録フォームに scope と grant を並べるだけでは、成立しない組み合わせを作れてしまいます (例: acp:checkout × authorization_code、会員同意が前提の ucp:identity × client_credentials)。また 1 クライアントに ACP と UCP を混在させると、受注に記録される Order.agent_id (= クライアント識別子) から事業者を特定できず、失効・監査を事業者単位で行えません。そこで登録導線自体を protocol ごとに分離しました。

管理画面: クライアントシークレットの扱い

league は保存時にハッシュ化せず、初回のトークン取得成功時に bcrypt へ日和見アップグレードします (ClientRepository::validateClient)。そのため一覧に表示される値は、一度使われた後は事業者へ渡せないハッシュになります。

  • 登録直後にシークレットを 1 度だけ表示する完了画面を追加 (Cache-Control: no-store, private)。登録画面にもその旨を明記。
  • 一覧では ACP/UCP 用クライアントのシークレットを再表示せず - 表示 (理由をツールチップで提示)。public client (DCR 登録等) も同様に - 表示に統一。
  • 列追加に伴い一覧の列幅を <colgroup> で明示 (見出しの折り返し解消)。

テスト

  • AgentCommerceAccessTokenHandlerTest: scope 付与 / 不正トークンの BadCredentialsException 正規化。
  • AgentCommerceClientControllerTest: protocol 別の scope 提示 / grant が client_credentials に固定される / protocol を跨いだ scope はフォームバイパスでも拒否 / ucp:identity は付与不可 / 完了画面で平文を 1 度だけ提示し no-store が付く / 一覧にシークレットが出ない。
  • OAuthControllerTest: 名称の永続化を追加。

設計メモ

  • 本体は api4 の具象クラスに依存せず、Symfony 標準 AccessTokenHandlerInterface のみに依存する疎結合設計。本 PR がその口を提供する。
  • MCP の PR (feat: MCP サーバ用 OAuth2 scope と firewall を追加 #190) はマージ済みで、本ブランチは upstream/4.4 を取り込み済み。scopes.available は MCP read 4 件と agentic 6 件が共存する。MCP discovery の scopes_supportedMcpTokenService::AVAILABLE_SCOPES を唯一のソースにしているため、agentic scope の追加による汚染はない
  • 公開 DCR (POST /register) は grant を authorization_code+refresh_token、scope を MCP read にハードクランプするため、client_credentials を全体で有効化しても匿名登録クライアントが machine トークンを取得することはない
  • 会員 ID 連携 (authorization_code / ucp:identity) は Customer(会員) に紐づく OAuth2 authorization_code フロー (ID 連携) のサポート #189 の範囲。

検証

ローカルで EC-CUBE 4.4 + 本 PR の api44 + sample-payment-plugin を共存させ、HTTPS (symfony CLI) 上で確認しました。

  • ACP/UCP チェックアウト E2E: 管理画面の「ACP 新規追加」から登録したクライアントで POST /token (client_credentials) → 実 JWT を取得 → e2e/agent/acp-checkout.php PASS (31 assertions) / e2e/agent/ucp-checkout.php PASS (23 assertions)。3DS 中断・再開、決済拒否を含む complete まで通過し、受注の agent_protocol / agent_id が期待どおり記録されることを確認。
  • scope 越境の拒否: ACP クライアントで ucp:checkout を要求すると invalid_scope
  • MCP との共存: 本体 MCP (MCPサーバ実装 ec-cube#6832) をローカルにマージした環境で、OAuth 自動ディスカバリ (RFC 9728 → RFC 8414 → DCR → authorization_code + PKCE) のみで MCP セッションを確立し tools/list (11 ツール) / tools/call まで到達。refresh_token のローテーションと旧トークン失効も確認。エージェントコマース側の E2E もこの状態で PASS。
  • 管理画面: 各画面をブラウザ (playwright) で描画確認。ツールチップの実表示を DOM で実測、コンソールエラー・警告 0 件。
  • 静的解析 / テスト: PHPStan level 6 No errors / PHPUnit 127 tests, 342 assertions, 0 failures (deprecation 3 件はいずれも既存コード由来)。

🤖 Generated with Claude Code

…nHandler を追加 (EC-CUBE#188)

エージェントコマース (ACP/UCP) の machine-to-machine インバウンド認証を有効化する。

- client_credentials grant を有効化 (services.yaml・ClientType の grants 選択肢)
- scope レジストリに acp:/ucp: の 6 scope を登録 (<protocol>:<capability> 規約)
- AgentCommerceAccessTokenHandler: Symfony 標準 AccessTokenHandlerInterface 実装。
  league ResourceServer で Bearer JWT を検証 (公開鍵署名/期限/失効) し、付与 scope を
  UserBadge attributes へ載せる。本体 AgentCommerceOAuth2Authenticator が依存する口を提供。
- ハンドラのユニットテスト (scope 付与 / 不正トークンの BadCredentials 正規化)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 843b9884-29f4-427c-bf52-14bc1d2579af

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…erce

# Conflicts:
#	Resource/config/services.yaml
client_credentials と acp:/ucp: scope を汎用の OAuth クライアント登録フォームに
足しただけでは、 成立しない組み合わせ (例: acp:checkout × authorization_code や、
会員同意が前提の ucp:identity × client_credentials) を作れてしまう。 また 1 つの
クライアントに ACP と UCP を混在させると、 受注に記録される agent_id
(= クライアント識別子) から事業者を特定できず、 失効・監査を事業者単位で行えない。

そこで注意文言ではなく登録導線自体を protocol ごとに分ける。

- 「ACP 新規追加」「UCP 新規追加」ボタンと専用画面を追加。 grant は
  client_credentials 固定、 scope は当該 protocol のものだけを提示し、
  リダイレクト URI は入力させない
- ucp:identity は Customer subject の authorization_code が前提のため
  この導線から除外 (eccube-api4#189 landing 後に会員同意を伴う別導線で追加)
- 汎用フォーム (ClientType) は GraphQL 用に戻し、 acp:/ucp: と
  client_credentials の選択肢を削除
- クライアントに名称を持たせ (Client::name)、 一覧へ名称列を追加。
  どの事業者向けのクライアントかを一覧で追える
- 契約テスト: grant 固定 / protocol 跨ぎの scope 拒否 / ucp:identity 不可

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nanasess
nanasess force-pushed the feature/agentic-commerce branch from f1a5322 to a2c388d Compare August 5, 2026 06:37
@nanasess nanasess changed the title feat(agent-commerce): client_credentials + agentic scope + AccessTokenHandler を追加 (#188) feat(agent-commerce): エージェントコマース用 OAuth2(client_credentials / scope / AccessTokenHandler / クライアント登録導線)を追加 (#188) Aug 5, 2026
@nanasess
nanasess marked this pull request as ready for review August 5, 2026 06:59

@ttokoro20240902 ttokoro20240902 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コードベースと静的確認(league/oauth2-server-bundle 1.x・symfony/form・EC-CUBE 本体 4.4 の実装との照合)によるレビューです。動作確認は行っていません。CI は全ジョブ green を確認済みです。

疎結合設計(本体は Symfony 標準 AccessTokenHandlerInterface にのみ依存)と、protocol ごとに登録導線を分けて成立しない grant×scope の組み合わせを作れなくする方針は妥当だと思います。

指摘は 13 件、各行のインラインコメントに置きました。内訳は 本PRでの修正をお願いしたいもの 4 件 / 別Issue・別PRが妥当と考えるもの 4 件 / コメント・注意書きの追記で足りるもの 5 件 です。リリースブロッカーに相当するものはありません。

最優先は AgentCommerceClientType の identifier に予約識別子 mcp_pat を弾く制約がない件で、登録されると管理画面の一覧から消えて削除不能になり、かつ MCP PAT が ACP クライアント配下で発行されます。

PR 説明について 1 点

「公開 DCR は grant をハードクランプするため、client_credentials を全体で有効化しても匿名登録クライアントが machine トークンを取得することはない」の部分ですが、根拠としては不正確です。実際に効いている主たる防御は league 側にあり、DCR のクランプはそれとは独立した二重防御です。またこの記述だと「既存レコードは安全」と読めてしまいますが、そうとは限りません(services.yaml のインラインコメント参照)。実装の妥当性は変わらないので、説明の書き換えだけお願いできればと思います。

Comment thread Form/Type/Admin/AgentCommerceClientType.php
Comment thread Form/Type/Admin/AgentCommerceClientType.php
Comment thread Resource/config/services.yaml
Comment thread Resource/template/admin/OAuth/agent_commerce_client_issued.twig
Comment thread Resource/config/services.yaml
Comment thread Form/Type/Admin/AgentCommerceClientType.php
Comment thread Resource/template/admin/OAuth/agent_commerce_client_issued.twig Outdated
Comment thread Controller/Admin/AgentCommerceClientController.php
Comment thread Security/AgentCommerceAccessTokenHandler.php
Comment thread Resource/config/services.yaml
PR EC-CUBE#191 のレビュー指摘のうち、実害が限定的で対応コストの低い 5 件に対応する。

- クライアントシークレットに最小長 (32 文字) を追加。client_credentials では
  シークレットが唯一の認証情報のため。既定値は sha512 hex (128 文字) なので、
  管理者が手で書き換えた場合にだけ効く。汎用の ClientType も token エンドポイントの
  認証材料はシークレットのみなので同条件に揃える。
- scopes の Assert\All + Assert\Choice のコメントを実機構に合わせて訂正。
  choices 外の値を実際に弾いているのは ChoiceType 自身 (PRE_SUBMIT で submitted data
  から除去し POST_SUBMIT で FormError を積む) で、本制約は多層防御として残す。
- 発行完了画面のコピーボタンに document.execCommand フォールバックを追加。
  navigator.clipboard は secure context 以外では undefined になり TypeError で
  無言のコピー失敗になる。一覧画面 (index.twig) の既存実装に揃える。
- AgentCommerceAccessTokenHandler の docblock に、role_prefix による scope → role
  変換を経由しない旨を明記。access_control や is_granted() で ROLE_OAUTH2_<SCOPE> を
  期待した人が確実に踏むため。
- AccessTokenHandlerInterface の alias がアプリ全体に効く旨を services.yaml に注記。
  実際に EC-CUBE 本体の services_test.yaml が同じ ID にスタブを登録しており、
  本プラグイン導入下では解決先が入れ替わる。

検証: PHPUnit 128 tests / 343 assertions / 0 failures、PHPStan level 6 No errors、
php-cs-fixer 0 件。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nanasess

Copy link
Copy Markdown
Collaborator Author

@ttokoro20240902

「公開 DCR は grant をハードクランプするため、client_credentials を全体で有効化しても匿名登録クライアントが machine トークンを取得することはない」の部分ですが、根拠としては不正確です。

本プラグイン機能に起因する不具合ではないので問題ないと思います

@ttokoro20240902 ttokoro20240902 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@nanasess
nanasess merged commit bca124e into EC-CUBE:4.4 Aug 13, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants