B2Bパスキー アーキテクチャ設計書
EC-CUBE 管理画面向けの B2B パスキー認証 (WebAuthn / FIDO2) のフローと EF Core モデル設計。
概要
EC-CUBE管理画面向けのパスキー認証(WebAuthn/FIDO2)の詳細設計を定義する。
目的
- 管理画面のフィッシング対策
- 店舗管理者の設定負担軽減(外部IdP設定不要)
- フリーミアム提供(5ユーザーまで無料)によるEcAuth導入促進
スコープ
- B2Bパスキー認証API(EcAuth側)
- EC-CUBEプラグイン連携設計
- データモデル設計
WebAuthn/FIDO2 概要
パスキーとは
パスキー(Passkey)は、FIDO2/WebAuthn標準に基づく認証方式。公開鍵暗号を使用し、フィッシング耐性を持つ。
主要な特徴
- フィッシング耐性: RP ID(ドメイン)検証により、偽サイトでは認証不可
- パスワードレス: パスワードの記憶・管理が不要
- デバイス連携: Touch ID、Windows Hello、セキュリティキー等に対応
用語
| 用語 | 説明 |
|---|---|
| RP (Relying Party) | 認証を要求するサービス(EC-CUBEサイト) |
| RP ID | RPを識別するドメイン名(shop.example.com) |
| Credential | 認証に使用する公開鍵クレデンシャル |
| Challenge | リプレイ攻撃防止用のランダム値 |
| SignCount | 認証回数カウンター(リプレイ攻撃検出用) |
| Attestation | デバイスの証明情報 |
アーキテクチャ全体像
flowchart TB
subgraph ECCUBE["EC-CUBE サイト (shop.example.com)"]
subgraph Plugin["EC-CUBEプラグイン"]
P1["管理画面ログインUI拡張"]
P2["パスキー管理画面"]
P3["WebAuthn JavaScript API呼び出し"]
P4["本人確認(パスワード再入力)"]
end
WebAuthn["navigator.credentials.*
(RP ID = shop.example.com)"]
end
subgraph EcAuth["EcAuth ({tenant}.ec-auth.io)"]
subgraph API["B2Bパスキー API"]
A1["/v1/b2b/passkey/register/options"]
A2["/v1/b2b/passkey/register/verify"]
A3["/v1/b2b/passkey/authenticate/options"]
A4["/v1/b2b/passkey/authenticate/verify"]
end
subgraph DB["データベース"]
D1["B2BUser"]
D2["B2BPasskeyCredential"]
D3["WebAuthnChallenge"]
end
end
Plugin <--> WebAuthn
ECCUBE <-->|API通信| EcAuth
API <--> DB
データモデル設計
注意
注意
以下のデータモデルは概念設計を示すものです。実装時には、EF
Coreの規約に従い、適切な外部キープロパティ(例:
B2BUserId)やFluent
API設定が追加されます。ナビゲーションプロパティが定義されている場合、EF
Coreは外部キーを自動生成します。
B2BUser
管理者ユーザーの最小限情報を保持。
public class B2BUser
{
public int Id { get; set; }
public string Subject { get; set; } // UUID(EcAuth内部識別子)
public string? ExternalId { get; set; } // EC-CUBEのlogin_id等
public string UserType { get; set; } // "admin", "staff" 等
public int OrganizationId { get; set; } // マルチテナント
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }
// Navigation properties
public Organization Organization { get; set; }
public ICollection<B2BPasskeyCredential> PasskeyCredentials { get; set; }
}B2BPasskeyCredential
パスキー(公開鍵クレデンシャル)情報を保持。
public class B2BPasskeyCredential
{
public int Id { get; set; }
public string B2BSubject { get; set; } // B2BUser.Subject
public byte[] CredentialId { get; set; } // WebAuthn credential ID
public byte[] PublicKey { get; set; } // COSE形式公開鍵
public uint SignCount { get; set; } // リプレイ攻撃防止カウンター
public string? DeviceName { get; set; } // "MacBook Pro", "iPhone" 等
public Guid AaGuid { get; set; } // Authenticator Attestation GUID
public string[] Transports { get; set; } // ["internal", "usb", "nfc", "ble"] 等
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? LastUsedAt { get; set; }
// Navigation property
public B2BUser B2BUser { get; set; }
}WebAuthnChallenge
認証チャレンジの一時保存。
public class WebAuthnChallenge
{
public int Id { get; set; }
public string Challenge { get; set; } // Base64URL形式
public string SessionId { get; set; } // セッション識別子
public string Type { get; set; } // "registration" or "authentication"
public string UserType { get; set; } // "b2b" or "b2c"
public string? Subject { get; set; } // ユーザーSubject
// - UserType == "b2b": 登録・認証ともに必須(既存ユーザーへのパスキー追加)
// - UserType == "b2c" && Type == "authentication": 必須
// - UserType == "b2c" && Type == "registration": null許容(JITプロビジョニング)
public string? RpId { get; set; } // EC-CUBEサイトのドメイン
public int ClientId { get; set; } // クライアントID
public DateTimeOffset ExpiresAt { get; set; } // 5分で期限切れ
public DateTimeOffset CreatedAt { get; set; }
}ER図
erDiagram
Organization ||--o{ B2BUser : has
B2BUser ||--o{ B2BPasskeyCredential : has
Client ||--o{ WebAuthnChallenge : has
APIエンドポイント設計
登録フロー
POST /v1/b2b/passkey/register/options
登録オプション(チャレンジ)を生成。
リクエスト:
{
"client_id": "your_client_id",
"rp_id": "shop.example.com",
"b2b_subject": "uuid-of-admin-user",
"device_name": "MacBook Pro"
}レスポンス:
{
"session_id": "random-session-id",
"challenge": "base64url-encoded-challenge",
"rp": {
"id": "shop.example.com",
"name": "ショップ名"
},
"user": {
"id": "base64url-encoded-user-id",
"name": "admin@shop.example.com",
"displayName": "管理者"
},
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 },
{ "type": "public-key", "alg": -257 }
],
"authenticatorSelection": {
"authenticatorAttachment": "platform",
"residentKey": "preferred",
"userVerification": "preferred"
},
"timeout": 60000,
"attestation": "none"
}POST /v1/b2b/passkey/register/verify
パスキー登録を完了。
リクエスト:
{
"session_id": "random-session-id",
"client_id": "your_client_id",
"response": {
"id": "credential-id",
"rawId": "base64url-encoded-raw-id",
"response": {
"attestationObject": "base64url-encoded-attestation",
"clientDataJSON": "base64url-encoded-client-data"
},
"type": "public-key"
},
"device_name": "MacBook Pro"
}レスポンス:
{
"success": true,
"credential_id": "base64url-encoded-credential-id"
}認証フロー
POST /v1/b2b/passkey/authenticate/options
認証オプション(チャレンジ)を生成。
リクエスト:
{
"client_id": "your_client_id",
"rp_id": "shop.example.com"
}レスポンス:
{
"session_id": "random-session-id",
"challenge": "base64url-encoded-challenge",
"rpId": "shop.example.com",
"allowCredentials": [
{
"id": "base64url-encoded-credential-id",
"type": "public-key",
"transports": ["internal"]
}
],
"userVerification": "preferred",
"timeout": 60000
}POST /v1/b2b/passkey/authenticate/verify
パスキー認証を検証し、認可コードを発行。
リクエスト:
{
"session_id": "random-session-id",
"client_id": "your_client_id",
"redirect_uri": "https://shop.example.com/admin/ecauth/callback",
"state": "random-state",
"response": {
"id": "credential-id",
"rawId": "base64url-encoded-raw-id",
"response": {
"authenticatorData": "base64url-encoded-auth-data",
"clientDataJSON": "base64url-encoded-client-data",
"signature": "base64url-encoded-signature"
},
"type": "public-key"
}
}レスポンス:
{
"redirect_url": "https://shop.example.com/admin/ecauth/callback?code=xxx&state=xxx"
}管理API
GET /v1/b2b/passkey/list
登録済みパスキー一覧を取得。
DELETE /v1/b2b/passkey/{credentialId}
パスキーを削除。
認証フロー詳細
パスキー登録フロー
sequenceDiagram
participant Admin as 管理者
participant ECCUBE as EC-CUBE
participant EcAuth as EcAuth
participant Auth as Authenticator
Admin->>ECCUBE: ログイン済み
Admin->>ECCUBE: 「パスキー追加」
ECCUBE->>Admin: パスワード確認
Admin->>ECCUBE: パスワード入力
ECCUBE->>EcAuth: register/options
EcAuth->>ECCUBE: challenge
ECCUBE->>Auth: navigator.credentials.create()
Auth-->>Auth: 生体認証/PIN
Auth->>ECCUBE: credential
ECCUBE->>EcAuth: register/verify
EcAuth->>ECCUBE: success
ECCUBE->>Admin: 登録完了
パスキー認証フロー
sequenceDiagram
participant Admin as 管理者
participant ECCUBE as EC-CUBE
participant EcAuth as EcAuth
participant Auth as Authenticator
Admin->>ECCUBE: 管理画面アクセス
Admin->>ECCUBE: 「パスキーでログイン」
ECCUBE->>EcAuth: authenticate/options
EcAuth->>ECCUBE: challenge + allowCredentials
ECCUBE->>Auth: navigator.credentials.get()
Auth-->>Auth: 生体認証/PIN
Auth->>ECCUBE: assertion
ECCUBE->>EcAuth: authenticate/verify
EcAuth->>ECCUBE: redirect_url (code, state)
ECCUBE->>Admin: redirect
ECCUBE->>EcAuth: /v1/token (code)
EcAuth->>ECCUBE: id_token, access_token
ECCUBE->>Admin: ログイン完了
EC-CUBEプラグイン連携設計
RP ID設計
WebAuthnでは、RP IDはユーザーがアクセスしているドメインと一致する必要がある。
ユーザーがアクセス: https://shop.example.com/admin/
RP ID: shop.example.com
重要
navigator.credentials.create()
および navigator.credentials.get()
はEC-CUBEサイト上で実行する必要がある。
プラグイン実装範囲
| 機能 | EC-CUBEプラグイン | EcAuth |
|---|---|---|
| ログインUI | ✓ | - |
| パスキー管理画面 | ✓ | - |
| WebAuthn API呼び出し | ✓ | - |
| 本人確認(パスワード) | ✓ | - |
| チャレンジ生成・検証 | - | ✓ |
| 公開鍵保存・管理 | - | ✓ |
| 署名検証 | - | ✓ |
| トークン発行 | - | ✓ |
Clientテーブル拡張
パスキー用に許可されるRP ID(ドメイン)を管理。
public class Client
{
// 既存フィールド...
// パスキー用:許可されるRP ID(ドメイン)一覧
public List<string> AllowedRpIds { get; set; } = new();
}セキュリティ考慮事項
チャレンジ管理
- 毎回ランダム生成(32バイト以上)
- 5分で期限切れ
- 使用後は即座に削除
SignCount検証
- 認証ごとにカウンターを検証
- カウンターが減少した場合はクローン検出として拒否
RP ID検証
- クライアントに登録されたAllowedRpIdsと一致確認
- フィッシングサイトでの認証を防止
本人確認(パスキー登録時)
- EC-CUBEプラグイン側でパスワード再入力を要求
- 検証成功後、一時トークン(5分有効)を発行
- EcAuth APIはトークンを検証してから登録処理
登録完了通知
- パスキー登録完了時にメール通知(推奨)
- デバイス名、登録日時、IPアドレスを記載
- 不正登録の早期発見