EcAuthDocs

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

Dockerfile:

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: WebAuthn CredentialCreateOptions
  • is_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.yamlbind%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(認証不要)

  1. codestate パラメータ受信
  2. セッションの ecauth_statehash_equals() で検証(使い捨て)
  3. EcAuthApiClient::exchangeToken() でトークン交換
  4. ID Token の sub クレームから b2b_subject 取得
  5. ecauth_subjectdtb_member 検索
  6. Symfony security.token_storage で管理者セッション確立
  7. Access Token をセッションに保存(パスキー管理画面で使用)
  8. 管理画面ホームへリダイレクト

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. 一覧画面リロード
Note
Note

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 側で検証

検証方法

  1. docker compose up -d で EC-CUBE 4.3 + プラグインを起動
  2. 管理画面でプラグイン設定を入力
    • 通常: Client ID / Client Secret のみ入力(EcAuth URL は https://api.ec-auth.io/platform/v1/client-resolve から自動解決)
    • ステージング検証時: ECAUTH_CLIENT_RESOLVE_URL を Azure デフォルトドメインに設定するか、「高度な設定」で EcAuth URL を直接入力
  3. 管理画面ログアウト → 「パスキーでログイン」で認証フロー確認
  4. パスキー管理画面でパスキー登録・一覧・削除を確認
  5. 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 エンティティに将来のフィールドを追加し、設定画面のタブで機能ごとに分離する想定。