EcAuthDocs

Account 管理アーキテクチャ

EcAuth サービスを利用する顧客(EC-CUBE 店舗オーナー等)のアカウントを管理する設計。accounts / stg-accounts の 2 つの Organization を本番 DB / 本番 App Service に同居させ、既存の B2B パスキー認証機構と既存テナント解決ロジックを無改修で流用する。

概要

EcAuth では既に B2C / B2B のユーザー管理 が定義されているが、これらは EcAuth を利用する顧客 EC-CUBE サイトのユーザー を扱うものであり、EcAuth サービスそのものの利用者(申込者・組織オーナー)を扱う層は未実装である。

本ドキュメントは、その第三の層である Account(EcAuth 管理者ユーザー) の設計を定義する。Issue #39 の申込 API 実装(Phase D)の前提となる。

ユーザー管理層の位置付け

対象 SubjectType エンティティ 認証方式
B2C EC-CUBE 顧客サイトのエンドユーザー B2C=0 EcAuthUser + ExternalIdpMapping 外部 IdP フェデレーション、パスキー
B2B EC-CUBE 管理画面ユーザー B2B=1 B2BUser + B2BPasskeyCredential パスキー
Account(本ドキュメント) EcAuth サービス利用者(組織オーナー) Account=2 Account + account_organization パスキー(B2B 流用)、将来は B2B SSO
設計の核心

Account は Account 管理専用の Organization に所属する B2BUser として表現する。これにより、既存の B2BUser / B2BPasskeyCredential / B2BPasskeyController を一切改修せずに認証機構として流用できる。Account 固有の補助情報(メールアドレス、表示名など)と「ユーザー N : N Organization」の関係性のみ、新規テーブルで追加する。


設計の基本方針

1. Account 用 Organization の二重配置

Account の認証ドメインを 2 つの Organization として本番 DB に同居させ、両方を本番 App Service に配置する。

用途codetenant_name配置先 DB配置先 App Serviceホスト
本番運用accountsaccounts本番 DB本番 App Serviceaccounts.ec-auth.io
EcAuth 開発側の検証stg-accountsstg-accounts本番 DB(同居)本番 App Service(カスタムドメイン同居)stg-accounts.ec-auth.io

両方を本番 App Service に同居させる理由は、Azure App Service のステージングスロットや staging App Service がカスタムサブドメインを十分にサポートできず、TenantMiddleware のサブドメイン解決がデフォルトテナントにフォールバックしてしまうため。本番側のカスタムドメイン機能は問題なく複数サブドメインを 1 つの App Service に同居させられる。

staging EcAuth App Service の役割

staging EcAuth App Service では、Account 関連スキーマのマイグレーション通過確認と、AccountOrganization 等のテナントレベル動作確認のみ行う。Account 機能の E2E 検証(パスキー登録〜申込フロー〜トークン発行)は本番 EcAuth の stg-accounts Organization で実施する。

2. 既存 B2B Passkey API の無改修流用

Account 用の認証 API は新規実装せず、既存の /v1/b2b/passkey/* エンドポイントを呼び出す。accounts.ec-auth.io または stg-accounts.ec-auth.io に対してリクエストすれば、既存の TenantMiddleware がサブドメインから tenant_name を抽出し、対応する Organization に解決される。

  • B2BPasskeyController — 無改修
  • B2BPasskeyService / B2BUserService — 無改修
  • B2BUser / B2BPasskeyCredential テーブル — 無改修
  • TenantMiddleware — 無改修

3. account テーブルの位置付け

既存の空 account テーブルを「Account の補助情報テーブル」として再定義する。認証情報は B2BUser + B2BPasskeyCredential が保持し、account は次の責務に絞る:

  • メールアドレス(EC-CUBE の dtb_member.login_id と同様の位置付け)
  • 表示名・確認状態など、認証本体に含まれない補助情報

1 つの Account は 1 つの B2BUserSubject (UUID) を共有する 1:1 関係とする。

4. account_organization 中間テーブル(N:N)

1 つの Account が複数の顧客 Organization を管理できる。account_organization 中間テーブルで「Account ↔ Organization のロール」を表現する。テナントクエリフィルター対象外(IgnoreQueryFilters で参照)として、Account から自分の管理対象 Org を横断的に引けるようにする。


データモデル

ER 図

erDiagram
    organization ||--o{ b2b_user : "has"
    organization ||--o{ account : "has (accounts / stg-accounts only)"
    organization ||--o{ client : "has"
    b2b_user ||--o{ b2b_passkey_credential : "has"
    b2b_user ||--|| account : "shares subject (accounts / stg-accounts only)"
    account ||--o{ account_organization : "owns"
    organization ||--o{ account_organization : "managed by"

    organization {
        int id PK
        string code UK
        string tenant_name
        string name
        bool is_sandbox
    }
    b2b_user {
        int id PK
        string subject UK "UUID"
        string external_id "email for Account"
        string user_type "account_owner / admin / staff"
        int organization_id FK
    }
    b2b_passkey_credential {
        int id PK
        string b2b_subject FK
        bytes credential_id UK
        bytes public_key
        uint sign_count
    }
    account {
        int id PK
        string subject UK "= b2b_user.subject"
        string email
        int organization_id FK "= accounts or stg-accounts"
        string display_name
        datetimeoffset email_verified_at
    }
    account_organization {
        string account_subject PK,FK
        int organization_id PK,FK
        string role "owner / admin / member"
        datetimeoffset created_at
    }
    client {
        int id PK
        string client_id UK
        int organization_id FK
        string subject_type "B2C / B2B / Account"
        string allowed_rp_ids
    }
        

Account テーブル拡張仕様

現状の account テーブル(id / email / password / created_at / updated_at)に対し、以下の変更を加える。

カラム変更備考
id維持int PK Identity
subject追加UUID。b2b_user.subject と同値。Alternate Key
email制約強化nvarchar(255)(organization_id, email) 複合ユニーク
password削除パスキー / SSO に統一。互換性を保つ必要なし(未使用)
organization_id追加accounts または stg-accounts Org を指す FK。OnDelete=Restrict
display_name追加nvarchar(255) nullable。管理画面表示用
email_verified_at追加datetimeoffset nullable。申込メール確認完了時刻
created_at / updated_at維持

EC-CUBE の dtb_member.login_id と同様、Account.emailB2BUser.external_id と同じ値を保持する運用ルールとする(accounts / stg-accounts Org スコープに限る)。B2BUser 側の (organization_id, external_id) 複合ユニーク制約がそのまま email のユニーク性を担保する。

Account C# モデル

[Table("account")]
public class Account : ISubjectProvider
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Column("id")]
    public int Id { get; set; }

    [Column("subject")]
    [MaxLength(255)]
    [Required]
    public string Subject { get; set; } = string.Empty;  // B2BUser.Subject と一致

    [Column("email")]
    [MaxLength(255)]
    [Required]
    public string Email { get; set; } = string.Empty;

    [Column("organization_id")]
    [Required]
    public int OrganizationId { get; set; }              // accounts / stg-accounts Org

    [Column("display_name")]
    [MaxLength(255)]
    public string? DisplayName { get; set; }

    [Column("email_verified_at")]
    public DateTimeOffset? EmailVerifiedAt { get; set; }

    [Column("created_at")]
    public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;

    [Column("updated_at")]
    public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;

    public Organization? Organization { get; set; }
    public ICollection<AccountOrganization> ManagedOrganizations { get; }
        = new List<AccountOrganization>();
}

AccountOrganization C# モデル

[Table("account_organization")]
public class AccountOrganization
{
    [Column("account_subject")]
    [MaxLength(255)]
    [Required]
    public string AccountSubject { get; set; } = string.Empty;  // Account.Subject FK

    [Column("organization_id")]
    [Required]
    public int OrganizationId { get; set; }                      // 管理対象 Org FK

    [Column("role")]
    [MaxLength(50)]
    [Required]
    public string Role { get; set; } = "owner";                  // owner / admin / member

    [Column("created_at")]
    public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;

    public Account? Account { get; set; }
    public Organization? Organization { get; set; }
}

主キーは (account_subject, organization_id) の複合キー。Fluent API で HasKey を指定する。

DbContext 設定

// EcAuthDbContext.OnModelCreating に追加

// Account: 所属 Org のテナント(accounts / stg-accounts)のみ参照可能
modelBuilder.Entity<Account>()
    .HasQueryFilter(a => a.Organization != null
        && a.Organization.TenantName == _tenantService.TenantName);

modelBuilder.Entity<Account>()
    .HasAlternateKey(a => a.Subject);

modelBuilder.Entity<Account>()
    .HasOne(a => a.Organization)
    .WithMany()
    .HasForeignKey(a => a.OrganizationId)
    .OnDelete(DeleteBehavior.Restrict);

modelBuilder.Entity<Account>()
    .HasIndex(a => new { a.OrganizationId, a.Email })
    .IsUnique();

// AccountOrganization: テナント横断(クエリフィルター対象外)
modelBuilder.Entity<AccountOrganization>()
    .HasKey(ao => new { ao.AccountSubject, ao.OrganizationId });

modelBuilder.Entity<AccountOrganization>()
    .HasOne(ao => ao.Account)
    .WithMany(a => a.ManagedOrganizations)
    .HasForeignKey(ao => ao.AccountSubject)
    .HasPrincipalKey(a => a.Subject)
    .OnDelete(DeleteBehavior.Cascade);

modelBuilder.Entity<AccountOrganization>()
    .HasOne(ao => ao.Organization)
    .WithMany()
    .HasForeignKey(ao => ao.OrganizationId)
    .OnDelete(DeleteBehavior.Restrict);
// ※ AccountOrganization にはテナントクエリフィルターを設定しない

Client.subject_type カラム追加

TokenController が認可コードを発行する際に発行先 SubjectType を決定するため、Client に種別カラムを追加する。これは将来の B2B SSO 拡張時にも有用である。

// Client.cs
[Column("subject_type")]
[Required]
public SubjectType SubjectType { get; set; } = SubjectType.B2C;
  • 既存の B2C 用 Client → SubjectType.B2C(デフォルト)
  • EC-CUBE 管理画面用 Client → SubjectType.B2B
  • ecauth-admin-console(後述)→ SubjectType.Account

認証フロー

Account 登録フロー(申込時)

sequenceDiagram
    autonumber
    participant U as 申込者
    participant W as ec-auth.io
(Cloudflare Pages) participant A as accounts.ec-auth.io
(本番 IdentityProvider) participant DB as 本番 EcAuth DB U->>W: 申込フォーム入力
(email, organization 情報) W->>A: POST /api/signup/request
{email, org_name, site_url, ...} A->>DB: SignupRequest 作成 (confirm_token) A-->>U: メール送信 (SendGrid)
確認リンク付き U->>A: GET /signup/confirm?token=... A->>A: token 検証 A->>DB: Account 作成 (Subject=UUID) A->>DB: B2BUser 作成 (Subject 共有, external_id=email,
user_type="account_owner", org=accounts) A->>DB: 顧客 Organization 作成
(本番用 / sandbox 用) A->>DB: Client 作成 (各 Org に対して) A->>DB: AccountOrganization 作成
(role=owner, Org ごとに 1 行) A-->>U: パスキー登録画面へ U->>A: POST /v1/b2b/passkey/register/options
(client_id=ecauth-admin-console) Note over A: 既存 B2BPasskeyController が処理
tenant=accounts として解決 A-->>U: 登録オプション返却 U->>A: POST /v1/b2b/passkey/register/verify A->>DB: B2BPasskeyCredential 作成 A-->>U: 完了 → 管理画面ログインへ

Account ログインフロー

sequenceDiagram
    autonumber
    participant U as Account
    participant C as accounts.ec-auth.io
(管理画面 SPA) participant A as IdentityProvider participant DB as EcAuth DB U->>C: アクセス C->>A: POST /v1/b2b/passkey/authenticate/options
(client_id=ecauth-admin-console) A->>DB: WebAuthnChallenge 作成 A-->>C: 認証オプション返却 C->>U: WebAuthn API 呼び出し U-->>C: 認証応答 (assertion) C->>A: POST /v1/b2b/passkey/authenticate/verify A->>DB: 署名検証 / SignCount 更新 A->>DB: AuthorizationCode 発行
(SubjectType=Account) A-->>C: redirect_uri?code=... C->>A: POST /v1/token (authorization_code) A->>DB: AccountOrganization 引き当て A-->>C: AccessToken + IDToken
(claims: managed_orgs=[...], role=...) Note over C: アクセストークンで管理画面 API へ
既存実装との差分

登録/認証 API 自体は完全に既存の B2BPasskeyController を流用する。差分が必要なのは (1) TokenController での SubjectType.Account 分岐の有効化(2) IDToken / AccessToken の claims に managed_orgs(管理対象 Org 一覧)を含める拡張 の 2 点のみ。

TokenController での SubjectType.Account 対応

現状 TokenController.cs:314 に「Account 等のサポートされていない SubjectType」コメントがある分岐を有効化する。

// TokenController で SubjectType.Account 分岐を追加
switch (authCode.SubjectType)
{
    case SubjectType.B2C:
        // 既存 EcAuthUser からトークン発行
        break;
    case SubjectType.B2B:
        // 既存 B2BUser からトークン発行
        break;
    case SubjectType.Account:
        var account = await _context.Accounts
            .FirstOrDefaultAsync(a => a.Subject == authCode.Subject);
        var managedOrgs = await _context.AccountOrganizations
            .IgnoreQueryFilters()
            .Where(ao => ao.AccountSubject == authCode.Subject)
            .Select(ao => new { ao.OrganizationId, ao.Role })
            .ToListAsync();
        return await _tokenService.IssueAccountTokenAsync(account, managedOrgs);
}

申込 API 設計(Phase D)

Issue #39 Phase D の申込フローを Account + Organization 自動作成として実装する。

エンドポイント

メソッドパス用途
POST/api/signup/request申込リクエスト(メール確認トークン発行 + SendGrid 送信)
POST/api/signup/confirmメール確認(Account + Organization + Client + AccountOrganization を一括作成)
GET/api/signup/status/{token}申込状況確認

これらの API は accounts.ec-auth.iostg-accounts.ec-auth.io の両方に露出する。サブドメインに応じて配下に作られる Account / Organization は自動的に対応するテナント(accounts or stg-accounts)にスコープされるため、申込フローの E2E 検証は stg-accounts 側で繰り返し実施できる。

申込リクエスト例

POST /api/signup/request
{
  "email": "owner@example.jp",
  "organization_name": "サンプル株式会社",
  "production_site_url": "https://example.jp",
  "sandbox_site_url": "https://staging.example.jp",
  "ec_cube_version": "4"
}

confirm 後に作成されるレコード

  • Account: email=owner@example.jp, subject=UUID, organization_id=accounts または stg-accounts の id
  • B2BUser: subject=Account.Subject, external_id=owner@example.jp, user_type="account_owner", organization_id=accounts または stg-accounts の id
  • Organization(本番): code=example-jp, is_sandbox=false
  • Organization(sandbox): code=example-jp-sandbox, is_sandbox=true
  • Client(本番 / sandbox 各 1 件): allowed_rp_ids に各サイトドメインを設定
  • AccountOrganization × 2 行: 各 Org に対して role=owner

これらは 1 つの DB トランザクション内で原子的に作成する。途中で失敗した場合はロールバックし、再申込を許可する。


テナント解決とホスト設計

ホスト名割り当て

静的サイト(申込ページ)と認証 API でホスト名を分離する。これは現行 TenantMiddleware の「サブドメイン先頭セグメント → tenant_name」ロジックを無改修で活用するための設計である。

用途申込サイト認証 / 管理画面 API解決される tenant_name配置先
本番 Account 管理ec-auth.io
(Cloudflare Pages)
accounts.ec-auth.ioaccounts本番 App Service
EcAuth 開発側の検証(同上 or 別途)stg-accounts.ec-auth.iostg-accounts本番 App Service(同居)
ローカル開発本番 stg-accounts.ec-auth.io を呼ぶstg-accounts
なぜ staging App Service を使わないか

Azure App Service の staging スロット / staging 専用 App Service は、カスタムサブドメインの追加にコスト・運用負荷がかかり、TenantMiddleware がサブドメインを抽出できない場合 DEFAULT_ORGANIZATION_TENANT_NAME にフォールバックしてしまう。本番 App Service には複数のカスタムドメインを併設できるため、accounts.ec-auth.iostg-accounts.ec-auth.io を同じ App Service 上に置けば、サブドメイン解決が staging でも素直に機能する。staging EcAuth App Service ではマイグレーション通過確認とテナントレベル動作確認のみ行う。

DNS / Cloudflare Pages 構成

ec-auth.io               CNAME → ecauth-website.pages.dev    (Cloudflare Pages)
www.ec-auth.io           CNAME → ecauth-website.pages.dev    (Cloudflare Pages)
accounts.ec-auth.io      CNAME → <本番 EcAuth Azure Web App>  (新規追加)
stg-accounts.ec-auth.io  CNAME → <本番 EcAuth Azure Web App>  (新規追加, 同じ App Service)

accounts.ec-auth.io / stg-accounts.ec-auth.io 用の DNS レコードは Issue #39 Phase B(ecauth-infrastructure / Cloudflare Pages Terraform)で追加する。Azure 側は本番 App Service の Custom Domains に 2 件追加し、各々に SSL 証明書を設定する。

パスキー RP ID 設計

本番用 Client(ecauth-admin-console)と stg-accounts 用 Client それぞれに AllowedRpIds を設定する:

Client所属 OrgRP IDAllowedRpIds
ecauth-admin-consoleaccountsaccounts.ec-auth.io["accounts.ec-auth.io"]
ecauth-admin-console-stgstg-accountsstg-accounts.ec-auth.io["stg-accounts.ec-auth.io"]

RP ID を ec-auth.io(eTLD+1)にすると accountsstg-accounts でパスキー credential が共有されてしまうため、RP ID はサブドメイン単位で分離する。これにより本番 Account のパスキーが誤って stg-accounts 検証で使えないようガードできる。


is_sandbox の取り扱い

Organization.is_sandbox「EcAuth 本番 DB 内で、EC-CUBE 顧客側のテスト店舗 Organization を本番店舗 Organization と区別する」ためのフラグである。試用期間や課金状態の表現には使わない(必要なら別カラムを設ける)。

3 層のテスト/本番分離

分離手段
EcAuth 自体の検証(開発チーム)Organization code プレフィックス stg-stg-accounts Org(本番 DB 同居)
EC-CUBE 顧客店舗の環境(本番 DB 内)Organization.is_sandboxOrg A (本番店舗) / Org B (テスト店舗、本番 Account から見える)
試用期間(将来)別カラム(例: trial_until未実装

各 Organization の is_sandbox 値

  • accounts Org → false(実運用の認証ドメイン)
  • stg-accounts Org → false(EcAuth 開発チームの検証用だが、認証品質は production スペックなので sandbox 扱いではない)
  • 申込で作成される顧客 Org(本番)→ false
  • 申込で作成される顧客 Org(sandbox)→ true

顧客側 staging 店舗(staging.example.jp)の認証は 本番 accounts.ec-auth.io 経由で行う(顧客側に EcAuth staging エンドポイントは公開しない)。顧客の「本番店舗 / staging 店舗」の区別は、本番 accounts Org 配下に is_sandbox=true の子 Org として保持する。


B2B SSO 拡張余地

Account の認証は当面パスキーのみで提供するが、将来的に Azure Entra ID / Google Workspace 等の B2B SSO を追加する余地を残す。

B2C ではなく B2B SSO を採用する理由

  • Account の利用者は組織オーナーであり、企業 IdP(Entra ID 等)からの SSO ログインが自然
  • B2C フェデレーション(EcAuthUser + ExternalIdpMapping)はエンドユーザー向け JIT プロビジョニング設計のため、組織オーナーには不適
  • EC-CUBE 管理者の B2B SSO 化(要件定義 Phase 4)と同一機構を共有可能

将来追加するもの(本フェーズでは実装しない)

-- 新規テーブル(Phase 4 想定)
CREATE TABLE b2b_external_idp_mapping (
    id                INT IDENTITY PRIMARY KEY,
    b2b_subject       NVARCHAR(255) NOT NULL,    -- FK → b2b_user.subject
    external_provider NVARCHAR(100) NOT NULL,    -- azure_entra_id / google_workspace / ...
    external_subject  NVARCHAR(255) NOT NULL,
    created_at        DATETIMEOFFSET NOT NULL,
    CONSTRAINT UQ_b2b_external_idp UNIQUE (external_provider, external_subject)
);

このテーブルが追加されれば、Account も同じ仕組みで Entra ID 等を経由した SSO ログインが可能になる(accounts / stg-accounts Org の B2BUser に対する SSO 連携として)。

現フェーズで矛盾を生まないための準備

  1. Client.subject_type カラム導入により、AuthorizationCallback が将来 B2C / B2B / Account を分岐させる責務の置き場所を確立
  2. SubjectType.Account のトークン発行経路を TokenController に明示
  3. ExternalIdpMapping を B2BUser に後付け拡張せず、新規 b2b_external_idp_mapping を追加する方針を明記(既存テーブル改修を避ける)

マイグレーション計画

必要なマイグレーション

  1. UpdateAccountTable
    • password カラム削除
    • subject, organization_id, display_name, email_verified_at カラム追加
    • (organization_id, email) 複合ユニークインデックス
    • subject ユニークインデックス(Alternate Key)
    • FK: account.organization_id → organization.id
  2. AddAccountOrganizationTable
    • account_organization テーブル新規作成
    • 主キー: (account_subject, organization_id)
    • FK: account_subject → account.subject(Cascade)、organization_id → organization.id(Restrict)
  3. AddSubjectTypeToClient
    • client.subject_type カラム追加(デフォルト B2C=0
    • 既存 B2B 用 Client は別途 SQL で B2B=1 に更新
  4. SeedAccountsOrganizations
    • accounts Organization レコード投入
    • stg-accounts Organization レコード投入
    • ecauth-admin-console Client 投入(accounts Org, subject_type=Account, allowed_rp_ids=["accounts.ec-auth.io"]
    • ecauth-admin-console-stg Client 投入(stg-accounts Org, subject_type=Account, allowed_rp_ids=["stg-accounts.ec-auth.io"]
マイグレーション設計ルール

EcAuth リポジトリの CLAUDE.md に記載のとおり、migrationBuilder.Sql() でカラムを参照する DML は EXEC() 動的 SQL でラップすること。password カラム削除前に他のマイグレーションで参照が残っていないか確認すること。

Seeder と環境変数

OrganizationClientSeeder を拡張、または専用 AccountsOrganizationSeeder を新設して accounts / stg-accounts の 2 Org とそれぞれの Client を投入する。1Password の項目構成は ecauth-production-app に以下を追加:

# 本番 accounts Org
ACCOUNTS_CLIENT_ID=ecauth-admin-console
ACCOUNTS_CLIENT_SECRET=<1Password 管理>
ACCOUNTS_ALLOWED_RP_IDS=accounts.ec-auth.io
ACCOUNTS_REDIRECT_URI=https://accounts.ec-auth.io/auth/callback

# 開発チーム検証用 stg-accounts Org
STG_ACCOUNTS_CLIENT_ID=ecauth-admin-console-stg
STG_ACCOUNTS_CLIENT_SECRET=<1Password 管理>
STG_ACCOUNTS_ALLOWED_RP_IDS=stg-accounts.ec-auth.io
STG_ACCOUNTS_REDIRECT_URI=https://stg-accounts.ec-auth.io/auth/callback

環境変数の配線ルールは リポジトリガイド の通り 4 箇所すべて(.env.dev.tpl / .env.staging.tpl / GitHub Actions / Terraform app_settings)に追加する。本番 Terraform app_settings のみに設定し、staging Terraform には設定しない(staging EcAuth App Service では Account 機能を扱わないため)。


セキュリティと運用上の注意

本番 DB 内に staging 検証用 Org が同居することの運用注意

項目運用方針
データ純度本番運用クエリ・統計は Organization.code NOT LIKE 'stg-%' で stg-accounts 配下を除外
課金stg-accounts 配下の Account / 子 Org は MAU 課金対象外。判定ロジックは code プレフィックスで実装
データクリーンアップstg-accounts 配下のテスト Account / Org は定期的にクリーンアップ運用が必要。自動削除スクリプトを別途用意
監査ログ本番監査ログに stg-accounts の操作が混入するため、運用アラート条件は tenant=stg-accounts を除外して設定
顧客側 staging 店舗顧客の staging 店舗(例: staging.example.jp)の認証は本番 accounts Org の is_sandbox=true 子 Org を使う。顧客側に stg-accounts は公開しない

Account 削除時の挙動

  • Account 削除時、対応する B2BUser も削除(Subject 共有のため整合性を保つ)
  • AccountOrganizationCascade で自動削除
  • 管理対象 Organization 本体は 削除しない(owner 1 件削除で顧客サイトが消えると事故になる)
  • 最後の owner Account を削除しようとした場合は API エラーとする(権限移譲を強制)

Account 管理画面の CSRF / CORS

  • 申込 API(/api/signup/*)は ec-auth.io / www.ec-auth.io からの CORS のみ許可
  • 管理画面 API(/v1/account/* 等、Phase E 以降)は accounts.ec-auth.io / stg-accounts.ec-auth.io のみ許可
  • SubjectType.Account の AccessToken でないと管理画面 API は呼び出せない
  • パスキー RP ID 分離(accounts.ec-auth.io / stg-accounts.ec-auth.io)により、本番 credential が stg-accounts で使用不可

権限境界

ロール権限
ownerOrg 削除、課金設定、他 Account 招待・削除、すべての操作
adminB2BUser 管理、Client 設定、他 Account 招待(owner 除く)
member閲覧のみ(将来用、MVP では未使用)

未決事項 / TODO

  1. パスワード認証の完全廃止: account.password 削除でリカバリ手段がパスキー再発行のみになる。マジックリンク(メール経由再認証)の併用要否を要検討
  2. 招待フロー: owner が他 Account を招待する API の設計(Phase E 以降)
  3. Account 課金プラン: accounts Org に紐づく課金エンティティの設計(別ドキュメント化予定)。stg-accounts 配下は除外
  4. 監査ログ: Account の操作ログをどこに保存するか(既存 AccessToken 監査と統合するか別系統か)
  5. EcAuth 自身の管理者(superadmin): EcAuth 運営者が顧客 Account をサポート目的で代行操作する必要があるか。必要なら account.role=superadmin 等の特別フラグが必要
  6. stg-accounts のデータクリーンアップ運用: 自動削除スクリプトの設計(cron + Azure Functions 等)

関連ドキュメント