EC-CUBE 4.3系 EcAuth B2Bパスキー認証プラグイン 実装計画
EC-CUBE 4 系向け EcAuth プラグインの実装計画。Plugin Configuration から OAuth2 クライアント連携まで。
概要
GitHub Issue #254 に基づき、EC-CUBE 4.3系管理画面にB2Bパスキー認証を統合するプラグインを新規開発する。
| 項目 | 値 |
|---|---|
| リポジトリ | EcAuth/ec-cube4-ecauth |
| プラグインコード | EcAuthLogin43(4.3系)/
EcAuthLogin42(4.2系)/
EcAuthLogin4(4.0-4.1系) |
| 初回リリーススコープ | B2Bパスキー認証 |
| 将来スコープ | B2Cソーシャルログイン、B2Cパスキー、B2B SSO(同一プラグイン) |
| Docker イメージ | ghcr.io/ec-cube/ec-cube-php:8.3-apache-4.3 |
| 開発方式 | Composer ローカルリポジトリ(symlink) |
| 検証環境 | EcAuth ステージング(organization_code: staging) |
Step 1: リポジトリ作成・Docker 開発環境
1.1 GitHub リポジトリ作成
gh repo create EcAuth/ec-cube4-ecauth --public \
--description "EC-CUBE 4系向け EcAuth 認証プラグイン(B2Bパスキー / B2Cソーシャルログイン)"ブランチ戦略:
main— 4.3系(EcAuthLogin43)、初回開発対象4.2— 4.2系(EcAuthLogin42)、後日対応4.0-4.1— 4.0-4.1系(EcAuthLogin4)、後日対応
1.2 ディレクトリ構成
ec-cube4-ecauth/
├── composer.json
├── PluginManager.php
├── EcAuthLoginEvent.php # TemplateEvent サブスクライバ
├── EcAuthLoginNav.php # 管理画面ナビゲーション
├── Controller/
│ ├── Admin/
│ │ ├── ConfigController.php # プラグイン設定画面
│ │ └── PasskeyController.php # パスキー管理画面(一覧/削除/パスワード確認)
│ ├── EcAuthCallbackController.php # 認証コールバック(認証不要)
│ └── PasskeyAuthController.php # パスキー認証/登録 API 中継
├── Entity/
│ ├── Config.php # plg_ecauth_login43_config
│ └── MemberTrait.php # dtb_member に ecauth_subject 追加
├── Form/Type/Admin/
│ └── ConfigType.php
├── Repository/
│ └── ConfigRepository.php
├── Service/
│ ├── EcAuthApiClient.php # EcAuth API HTTP クライアント(PSR-18)
│ ├── ClientResolveService.php # Client ID → Base URL 解決(/platform/v1/client-resolve)
│ └── PasskeyAuthService.php # パスキー認証ビジネスロジック
├── Resource/
│ ├── config/services.yaml
│ ├── locale/messages.ja.yaml
│ ├── template/admin/
│ │ ├── config.twig # 設定画面
│ │ ├── passkey_list.twig # パスキー管理画面
│ │ └── login_passkey.twig # ログイン画面スニペット
│ └── assets/js/
│ └── webauthn.js # WebAuthn API ヘルパー
├── Tests/ # テスト・静的解析設定
│ ├── specs/ # Playwright E2E テスト
│ │ ├── passkey_auth.spec.ts
│ │ ├── passkey_register.spec.ts
│ │ └── plugin_config.spec.ts
│ ├── .php-cs-fixer.dist.php # PHP CS Fixer 設定
│ └── rector.php # Rector 設定
├── .github/workflows/
│ ├── ci.yml # PHPStan, Rector, CS Fixer
│ ├── playwright.yml # E2E テスト
│ └── deploy.yml # リリースパッケージ
├── Dockerfile # EC-CUBE 4.3 ベース
├── docker-compose.yml # EC-CUBE 4.3 + PostgreSQL
├── docker-compose.override.yml # Composer ローカルリポジトリ設定
├── docker-entrypoint.sh # プラグインインストール・有効化
├── .env.tpl # 1Password テンプレート(staging 接続情報)
├── package.json
├── pnpm-lock.yaml
├── playwright.config.ts
├── phpstan.neon.dist
├── .gitignore
├── CLAUDE.md
├── README.md
└── LICENSE # LGPL-2.1-or-later
1.3 composer.json
{
"name": "ec-cube/ecauthlogin43",
"version": "1.0.0",
"description": "EC-CUBE 4.3系向け EcAuth 認証プラグイン",
"type": "eccube-plugin",
"license": "LGPL-2.1-or-later",
"extra": {
"code": "EcAuthLogin43",
"id": 999999
},
"require": {
"ec-cube/plugin-installer": "^2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.64",
"phpstan/phpstan": "^1.10",
"rector/rector": "^1.0"
},
"config": {
"allow-plugins": {
"ec-cube/plugin-installer": true
}
}
}1.4 Docker 開発環境
docker-compose.yml:
networks:
backend:
driver: bridge
volumes:
pg-database:
driver: local
services:
ec-cube:
build: .
ports:
- "8080:80"
- "8081:443"
environment:
APP_ENV: dev
DATABASE_URL: "postgresql://eccube:password@postgres:5432/eccube_db"
DATABASE_SERVER_VERSION: 15
depends_on:
postgres:
condition: service_healthy
networks:
- backend
extra_hosts:
- "host.docker.internal:host-gateway"
postgres:
image: postgres:15-alpine
environment:
TZ: Asia/Tokyo
POSTGRES_DB: eccube_db
POSTGRES_USER: eccube
POSTGRES_PASSWORD: password
ports:
- "15432:5432"
volumes:
- pg-database:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: pg_isready -U eccube -d eccube_db
interval: 3s
timeout: 3s
retries: 10Dockerfile:
FROM ghcr.io/ec-cube/ec-cube-php:8.3-apache-4.3
COPY docker-entrypoint.sh /docker-entrypoint-plugin.sh
RUN chmod +x /docker-entrypoint-plugin.sh
ENTRYPOINT ["/docker-entrypoint-plugin.sh"]
CMD ["apache2-foreground"]docker-entrypoint.sh:
#!/bin/bash
set -e
# Composer ローカルリポジトリ経由でプラグインをシンボリックリンクインストール
composer config repositories.ecauth '{"type": "path", "url": "/plugin"}'
bin/console eccube:composer:require ecauth/ec-cube4-ecauth
bin/console eccube:plugin:enable --code=EcAuthLogin43
exec docker-php-entrypoint "$@"docker-compose.override.yml:
services:
ec-cube:
volumes:
- ".:/plugin:cached"1.5 EcAuth ステージング環境との接続
検証は EcAuth ステージング環境を使用する。.env.tpl で
1Password から接続情報を注入:
ECAUTH_BASE_URL=op://EcAuth/eccube4-ecauth-plugin/base_url
ECAUTH_CLIENT_ID=op://EcAuth/eccube4-ecauth-plugin/client_id
ECAUTH_CLIENT_SECRET=op://EcAuth/eccube4-ecauth-plugin/client_secret
organization_code: staging
Step 2: プラグイン骨格・設定画面
2.1
Entity/Config.php — plg_ecauth_login43_config
| カラム | 型 | 説明 |
|---|---|---|
| id | int (PK) | |
| ecauth_base_url | string(1024), nullable | EcAuth Base URL(未入力時は Client ID から自動解決) |
| client_id | string(255) | Client ID(必須) |
| client_secret | string(255) | Client Secret(必須) |
| rp_id | string(255), nullable | RP ID(未設定時はリクエストホスト使用) |
2.2 Entity/MemberTrait.php
@EntityExtension("Eccube\Entity\Member") で
ecauth_subject (VARCHAR 255, nullable, unique) を追加。
2.3 Controller/Admin/ConfigController.php
- ルート:
/%eccube_admin_route%/ecauth_login43/config - フォーム: ConfigType(client_id と client_secret のみ必須、ecauth_base_url と rp_id は「高度な設定」内で任意)
- テンプレート:
@EcAuthLogin43/admin/config.twig(Bootstrap 5 collapse で「高度な設定」を折りたたみ) ecauth_base_urlが未入力で保存された場合、ClientResolveService::resolve(clientId)を呼んで/platform/v1/client-resolveから base_url を取得し Config に保存する- resolve 失敗(404 / ネットワークエラー)時は FormError で再表示して DB を更新しない
2.4 EcAuthLoginNav.php
「設定」メニュー配下:
- EcAuth 設定 → ConfigController
- パスキー管理 → PasskeyController
2.5 PluginManager.php
enable() で Config のデフォルト行を作成。
Step 3: EcAuth API 連携サービス
3.1 Service/EcAuthApiClient.php
PSR-18 (Psr\Http\Client\ClientInterface) 経由で EcAuth
API と通信。Guzzle への直接依存は排除し、services.yaml で
PSR-18 インタフェースを Guzzle にエイリアスする構成(EC-CUBE 4.2+
は本体が guzzlehttp/guzzle:^7 を依存として持つため)。
| メソッド | EcAuth エンドポイント | 認証 |
|---|---|---|
authenticateOptions(rpId, ?b2bSubject) |
POST /v1/b2b/passkey/authenticate/options |
client_id |
authenticateVerify(sessionId, redirectUri, ?state, response) |
POST /v1/b2b/passkey/authenticate/verify |
client_id |
registerOptions(rpId, b2bSubject, externalId, ?displayName, ?deviceName) |
POST /v1/b2b/passkey/register/options |
client_id + client_secret |
registerVerify(sessionId, response, ?deviceName) |
POST /v1/b2b/passkey/register/verify |
client_id + client_secret |
listPasskeys(accessToken) |
GET /v1/b2b/passkey/list |
Bearer Token |
deletePasskey(accessToken, credentialId) |
DELETE /v1/b2b/passkey/{credentialId} |
Bearer Token |
exchangeToken(code, redirectUri) |
POST /v1/token |
client_id + client_secret |
registerOptions のバリデーション制約(EcAuth PR #288 で追加):
b2b_subject: UUID 形式必須(Guid.TryParseで検証)display_name: 128文字以内device_name: 128文字以内
registerOptions のレスポンス:
session_id: セッション識別子options: WebAuthnCredentialCreateOptionsis_provisioned:trueの場合、EcAuth 側で B2BUser が JIT プロビジョニング(自動作成)された
3.2 Service/ClientResolveService.php
Client ID から Base URL を解決する専用サービス(EcAuthDocs#58 で導入)。
| メソッド | エンドポイント | 認証 |
|---|---|---|
resolve(clientId) |
GET /platform/v1/client-resolve?client_id=xxx |
なし(テナント横断 API) |
- Discovery URL:
services.yamlのbindで%env(default:ecauth_default_discovery_url:ECAUTH_CLIENT_RESOLVE_URL)%を注入- デフォルト:
https://api.ec-auth.io - ステージング・開発では環境変数
ECAUTH_CLIENT_RESOLVE_URLで Azure デフォルトドメイン (https://ecauth-staging-*.azurewebsites.net) や Docker 内 URL (https://localhost:8081) に切り替え
- デフォルト:
- レスポンス:
{ tenant_name, base_url, organization_name } - ConfigController が保存時に呼び出し、成功時はレスポンスの
base_urlを Config.ecauth_base_url に保存
3.3 Service/PasskeyAuthService.php
| メソッド | 役割 |
|---|---|
ensureB2BUser(Member) |
ecauth_subject が無ければ UUID 生成・dtb_member
に保存。EcAuth 側の B2BUser は register/options
呼び出し時に JIT プロビジョニングされる |
handleCallback(code, state, session, redirectUri) |
state 検証 → トークン交換 → Member 検索 → セッション確立 |
verifyPassword(Member, password) |
本人確認(UserPasswordHasherInterface) |
getRpId(Request) |
Config の rp_id またはリクエストホスト名 |
extractSubFromIdToken(idToken) |
JWT の sub クレームを抽出(Base64URL デコード、exp
検証付き) |
Step 4: 管理画面ログイン UI 拡張(パスキー認証)
4.1 認証フロー
[ブラウザ] 「パスキーでログイン」クリック
↓
[JS] POST /ecauth/passkey/authenticate/options
→ [EC-CUBE PasskeyAuthController] → [EcAuth API]
← session_id + WebAuthn options
↓
[JS] navigator.credentials.get() ← ブラウザ生体認証 UI
↓
[JS] POST /ecauth/passkey/authenticate/verify
→ [EC-CUBE PasskeyAuthController] → [EcAuth API]
← redirect_url(authorization code 付き)
↓
[ブラウザ] リダイレクト → /ecauth/callback?code=xxx&state=xxx
↓
[EC-CUBE EcAuthCallbackController]
POST /v1/token → ID Token + Access Token
ecauth_subject で Member 検索 → セッション確立
→ 管理画面ホーム
4.2 ルーティング設計
認証不要(管理画面 firewall の外に配置):
| パス | コントローラー | 用途 |
|---|---|---|
POST /ecauth/passkey/authenticate/options |
PasskeyAuthController | チャレンジ取得 |
POST /ecauth/passkey/authenticate/verify |
PasskeyAuthController | 署名検証→認可コード |
GET /ecauth/callback |
EcAuthCallbackController | コールバック→ログイン |
管理者ログイン必須(管理画面 firewall 内):
| パス | コントローラー | 用途 |
|---|---|---|
POST /%admin%/ecauth/passkey/register/options |
PasskeyAuthController | パスキー登録オプション |
POST /%admin%/ecauth/passkey/register/verify |
PasskeyAuthController | パスキー登録完了 |
GET /%admin%/ecauth/passkey/ |
PasskeyController | 一覧 |
DELETE /%admin%/ecauth/passkey/{id}/delete |
PasskeyController | 削除 |
POST /%admin%/ecauth/passkey/verify-password |
PasskeyController | 本人確認 |
GET /%admin%/ecauth_login43/config |
ConfigController | 設定画面 |
4.3 EcAuthLoginEvent.php
@admin/login.twig イベントで
addSnippet('@EcAuthLogin43/admin/login_passkey.twig')
を注入。
4.4 login_passkey.twig
- ログインフォーム直後に「パスキーでログイン」ボタンを DOM 操作で挿入
- HTTPS 環境 かつ
window.PublicKeyCredential対応時のみ表示 - webauthn.js を読み込んで認証フローを実行
4.5 Resource/assets/js/webauthn.js
- Base64URL エンコード/デコード
authenticate(optionsUrl, verifyUrl)—navigator.credentials.get()ラッパーregister(optionsUrl, verifyUrl)—navigator.credentials.create()ラッパー- CSRF トークン送信対応
Step 5: コールバック処理
Controller/EcAuthCallbackController.php
ルート: GET /ecauth/callback(認証不要)
codeとstateパラメータ受信- セッションの
ecauth_stateとhash_equals()で検証(使い捨て) EcAuthApiClient::exchangeToken()でトークン交換- ID Token の
subクレームから b2b_subject 取得 ecauth_subjectでdtb_member検索- Symfony
security.token_storageで管理者セッション確立 - Access Token をセッションに保存(パスキー管理画面で使用)
- 管理画面ホームへリダイレクト
Step 6: パスキー管理画面
Controller/Admin/PasskeyController.php
| ルート | 機能 |
|---|---|
GET /%admin%/ecauth/passkey/ |
一覧表示(EcAuth API /v1/b2b/passkey/list) |
DELETE /%admin%/ecauth/passkey/{id}/delete |
削除(EcAuth API DELETE /v1/b2b/passkey/{id}) |
POST /%admin%/ecauth/passkey/verify-password |
本人確認(パスワード再入力) |
passkey_list.twig
@admin/default_frame.twigを extends- パスキー一覧テーブル(デバイス名、登録日時、最終使用日時)
- 「パスキーを追加」ボタン → パスワード再入力モーダル → WebAuthn 登録
- 各行に「削除」ボタン
パスキー登録フロー(本人確認付き)
1. 「パスキーを追加」クリック
2. モーダルでパスワード再入力
3. POST verify-password
→ パスワード検証(UserPasswordHasherInterface)
→ ecauth_subject 未設定なら UUID 生成・dtb_member に保存
← b2b_subject(UUID)
4. POST register/options → EcAuth API
→ EcAuth 側で B2BUser の JIT プロビジョニング(未存在なら自動作成)
← session_id + WebAuthn options + is_provisioned
5. navigator.credentials.create() → ブラウザ生体認証 UI
6. POST register/verify → EcAuth API → 登録完了
7. 一覧画面リロード
EcAuth PR #288
により、register/options 呼び出し時に EcAuth 側で B2BUser
が自動作成されるため、プラグイン側は
ecauth_subject(UUID)の生成・保存のみ行えばよい。b2b_subject
は UUID 形式必須。
Step 7: テスト・CI/CD
静的解析 (ci.yml)
- PHPStan Level 6(PHP 8.3)
- Rector dry-run
- PHP CS Fixer dry-run
E2E テスト (playwright.yml)
Playwright WebAuthn 仮想認証器を使用。EcAuth ステージング環境に接続。
| テストファイル | テスト内容 |
|---|---|
| passkey_auth.spec.ts | パスキーログインフロー全体 |
| passkey_register.spec.ts | 登録・一覧・削除 |
| plugin_config.spec.ts | 設定画面 |
デプロイ / リリースパッケージ (deploy.yml)
GitHub Release 公開時にプラグインの tar.gz パッケージを自動ビルドし、Release Asset に添付する。 ProductReview-plugin のパターンを踏襲。
# .github/workflows/deploy.yml
name: Packaging for EC-CUBE Plugin
on:
release:
types: [ published ]
jobs:
deploy:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Packaging
working-directory: ../
run: |
rm -rf "$GITHUB_WORKSPACE/.github"
rm -rf "$GITHUB_WORKSPACE/tests"
rm -rf "$GITHUB_WORKSPACE/node_modules"
rm -f "$GITHUB_WORKSPACE/Dockerfile"
rm -f "$GITHUB_WORKSPACE/docker-compose.yml"
rm -f "$GITHUB_WORKSPACE/docker-compose.override.yml"
rm -f "$GITHUB_WORKSPACE/docker-entrypoint.sh"
rm -f "$GITHUB_WORKSPACE/package.json"
rm -f "$GITHUB_WORKSPACE/package-lock.json"
rm -f "$GITHUB_WORKSPACE/yarn.lock"
rm -f "$GITHUB_WORKSPACE/playwright.config.ts"
rm -f "$GITHUB_WORKSPACE/phpstan.neon.dist"
rm -f "$GITHUB_WORKSPACE/rector.php"
rm -f "$GITHUB_WORKSPACE/.php-cs-fixer.dist.php"
rm -f "$GITHUB_WORKSPACE/.env.tpl"
rm -f "$GITHUB_WORKSPACE/CLAUDE.md"
find "$GITHUB_WORKSPACE" -name "dummy" -delete
find "$GITHUB_WORKSPACE" -name ".git*" -and ! -name ".gitkeep" -print0 | xargs -0 rm -rf
chmod -R o+w "$GITHUB_WORKSPACE"
cd "$GITHUB_WORKSPACE"
tar cvzf ../${{ github.event.repository.name }}-${{ github.event.release.tag_name }}.tar.gz ./*
- name: Upload binaries to release
uses: softprops/action-gh-release@v2
with:
files: ${{ runner.workspace }}/${{ github.event.repository.name }}-${{ github.event.release.tag_name }}.tar.gzパッケージに含めるファイル:
- composer.json, PluginManager.php, EcAuthLoginEvent.php, EcAuthLoginNav.php
- Controller/, Entity/, Form/, Repository/, Service/
- Resource/ (config, locale, template, assets)
- LICENSE, README.md
パッケージから除外するファイル:
- .github/, Tests/, node_modules/
- Docker 関連(Dockerfile, docker-compose.*, docker-entrypoint.sh)
- 開発ツール設定(phpstan, rector, php-cs-fixer, playwright)
- .env.tpl, CLAUDE.md, package.json, pnpm-lock.yaml
セキュリティ
| 項目 | 対策 |
|---|---|
| CSRF | Symfony CSRF トークン(フォーム + AJAX ヘッダー) |
| state 検証 | セッション保存 + hash_equals() + 使い捨て |
| HTTPS | WebAuthn は HTTPS 必須。HTTP 時はボタン非表示 |
| client_secret | サーバーサイドのみ。JS に渡さない |
| レートリミット | EC-CUBE rate_limiter で認証 API 保護 |
| WebAuthn | RP ID/SignCount/チャレンジ期限は EcAuth 側で検証 |
検証方法
docker compose up -dで EC-CUBE 4.3 + プラグインを起動- 管理画面でプラグイン設定を入力
- 通常: Client ID / Client Secret のみ入力(EcAuth
URL は
https://api.ec-auth.io/platform/v1/client-resolveから自動解決) - ステージング検証時:
ECAUTH_CLIENT_RESOLVE_URLを Azure デフォルトドメインに設定するか、「高度な設定」で EcAuth URL を直接入力
- 通常: Client ID / Client Secret のみ入力(EcAuth
URL は
- 管理画面ログアウト → 「パスキーでログイン」で認証フロー確認
- パスキー管理画面でパスキー登録・一覧・削除を確認
- Playwright E2E テストで自動検証
将来の拡張(同一プラグイン内)
| Phase | 機能 | 追加ファイル |
|---|---|---|
| Phase 2 | B2C パスキー(フロント) | Controller/Front/PasskeyController, フロントテンプレート |
| Phase 3 | B2C ソーシャルログイン | Controller/Front/FederatedLoginController, 外部 IdP 設定 |
| Phase 4 | B2B SSO(Azure Entra ID 等) | Controller/Admin/SsoController, SSO 設定エンティティ |
Config エンティティに将来のフィールドを追加し、設定画面のタブで機能ごとに分離する想定。