Skip to content

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

Open
nanasess wants to merge 4 commits into
EC-CUBE:4.4from
nanasess:feature/agentic-commerce
Open

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
# Whether to enable the client credentials grant
enable_client_credentials_grant: false
# エージェントコマース (ACP/UCP) の machine-to-machine インバウンド認証で使用する (#188)。
enable_client_credentials_grant: true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

【中】client_credentials の全体有効化が grants 空の既存クライアントに波及する(別Issue提案)

ClientRepository::isGrantSupported()$client->getGrants() が空なら全 grant を許可します。そのため oauth2_client.grants が空のレコードは、これまで authorization_code(=同意経由)でしか使えなかったのに、同意なしで client_credentials トークンを取得できるようになります。scope は read / write のまま通ります。

ただし実際の攻撃面は限定的で、リリースブロッカーではないと判断しています。

  • public client は対象外: ClientCredentialsGrant!isConfidential()invalid_client で弾きます。DCR 登録クライアントと PAT クライアントはいずれも secret = null なので該当しません。
  • 本プラグインが作ったクライアントも対象外: ClientTypegrants は初版から NotBlank 必須で、api プラグイン経由では grants 空のレコードは生成されません。

残るのは手動投入 / 他プラグイン由来 / データ移行由来の、シークレットを持つ grants 空クライアントのみです。一律 backfill は破壊的なので避け、grants 空の confidential クライアントを検出するコマンド(または管理画面での警告表示)を別Issueに切り出すのが良さそうです。

@nanasess nanasess Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ClientRepository::isGrantSupported() が grants 空で全 grant を許可すること(vendor/league/oauth2-server-bundle/src/Repository/ClientRepository.php:117-119)、および攻撃面が限定的である根拠(public client は ClientCredentialsGrant.php:39-43invalid_clientClientTypegrants は初版 936a67f から NotBlank 必須)は、いずれもこちらでも確認しました。ご分析のとおりです。

影響も限定的なので本 PR のスコープ外とします

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

本 PR のスコープ外というご判断に同意します。

その上で、grants 空の confidential クライアントを検出する手段(コマンドまたは管理画面での警告)を別 Issue に切り出すかどうかだけ、意思決定をお願いできますか。不要という判断であればこのまま resolve します。

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
# attributes['scopes'] に載せて返す。本体は handler が無ければ 503 を返す疎結合設計。
Plugin\Api44\Security\AgentCommerceAccessTokenHandler:
autowire: true
Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface: '@Plugin\Api44\Security\AgentCommerceAccessTokenHandler'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

【低】グローバル alias である旨の注意書きが欲しい

本体が @?Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface を参照する契約なので、この ID で alias を張ること自体は妥当です。

ただしこの alias はアプリ全体に効くため、将来 access_token firewall や他プラグインが同インターフェースを autowire / alias すると、無言で ACP ハンドラに解決される(あるいは alias が上書きされて ACP 側が壊れる)可能性があります。上のコメントブロックに一言残しておくと事故防止になります。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ご指摘のとおりです。efc050d で注意書きを追加しました。

補足すると、この衝突はすでに現実に起きています。EC-CUBE 本体の app/config/eccube/services_test.yaml が同じ ID(Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface)にテスト用スタブを登録しているため、本プラグインを導入した環境では本体側テストの解決先が入れ替わります。この実例もコメントに含めました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

efc050d の注意書きを確認しました。app/config/eccube/services_test.yaml:147-152 が同じ ID に InMemoryAccessTokenHandler を alias している点も確認しています。

この衝突はコメントに残す以上の意味があるかもしれません。api44 を導入した環境で本体のテストを流すと、alias の解決順によっては core の ACP テストがスタブではなく実ハンドラを掴み、本体側テストが落ちる可能性があります。本体テストは通常プラグイン無しで走るため実害は限定的ですが、プラグイン同梱構成の CI では踏むはずです。本体側に確認用の Issue を立てておくのが良いと思いますが、いかがでしょうか。

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>
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