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 に配置する。
| 用途 | code | tenant_name | 配置先 DB | 配置先 App Service | ホスト |
|---|---|---|---|---|---|
| 本番運用 | accounts | accounts | 本番 DB | 本番 App Service | accounts.ec-auth.io |
| EcAuth 開発側の検証 | stg-accounts | stg-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 では、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 つの B2BUser と Subject (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.email は B2BUser.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.io と stg-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 の idB2BUser:subject=Account.Subject,external_id=owner@example.jp,user_type="account_owner",organization_id=accounts または stg-accounts の idOrganization(本番):code=example-jp,is_sandbox=falseOrganization(sandbox):code=example-jp-sandbox,is_sandbox=trueClient(本番 / 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.io | accounts | 本番 App Service |
| EcAuth 開発側の検証 | (同上 or 別途) | stg-accounts.ec-auth.io | stg-accounts | 本番 App Service(同居) |
| ローカル開発 | — | 本番 stg-accounts.ec-auth.io を呼ぶ | stg-accounts | — |
Azure App Service の staging スロット / staging 専用 App Service は、カスタムサブドメインの追加にコスト・運用負荷がかかり、TenantMiddleware がサブドメインを抽出できない場合 DEFAULT_ORGANIZATION_TENANT_NAME にフォールバックしてしまう。本番 App Service には複数のカスタムドメインを併設できるため、accounts.ec-auth.io と stg-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 | 所属 Org | RP ID | AllowedRpIds |
|---|---|---|---|
ecauth-admin-console | accounts | accounts.ec-auth.io | ["accounts.ec-auth.io"] |
ecauth-admin-console-stg | stg-accounts | stg-accounts.ec-auth.io | ["stg-accounts.ec-auth.io"] |
RP ID を ec-auth.io(eTLD+1)にすると accounts と stg-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_sandbox | Org A (本番店舗) / Org B (テスト店舗、本番 Account から見える) |
| 試用期間(将来) | 別カラム(例: trial_until) | 未実装 |
各 Organization の is_sandbox 値
accountsOrg →false(実運用の認証ドメイン)stg-accountsOrg →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 連携として)。
現フェーズで矛盾を生まないための準備
Client.subject_typeカラム導入により、AuthorizationCallbackが将来 B2C / B2B / Account を分岐させる責務の置き場所を確立SubjectType.Accountのトークン発行経路をTokenControllerに明示ExternalIdpMappingを B2BUser に後付け拡張せず、新規b2b_external_idp_mappingを追加する方針を明記(既存テーブル改修を避ける)
マイグレーション計画
必要なマイグレーション
UpdateAccountTablepasswordカラム削除subject,organization_id,display_name,email_verified_atカラム追加(organization_id, email)複合ユニークインデックスsubjectユニークインデックス(Alternate Key)- FK:
account.organization_id → organization.id
AddAccountOrganizationTableaccount_organizationテーブル新規作成- 主キー:
(account_subject, organization_id) - FK:
account_subject → account.subject(Cascade)、organization_id → organization.id(Restrict)
AddSubjectTypeToClientclient.subject_typeカラム追加(デフォルトB2C=0)- 既存 B2B 用 Client は別途 SQL で
B2B=1に更新
SeedAccountsOrganizationsaccountsOrganization レコード投入stg-accountsOrganization レコード投入ecauth-admin-consoleClient 投入(accountsOrg,subject_type=Account,allowed_rp_ids=["accounts.ec-auth.io"])ecauth-admin-console-stgClient 投入(stg-accountsOrg,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 共有のため整合性を保つ)AccountOrganizationはCascadeで自動削除- 管理対象
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 で使用不可
権限境界
| ロール | 権限 |
|---|---|
owner | Org 削除、課金設定、他 Account 招待・削除、すべての操作 |
admin | B2BUser 管理、Client 設定、他 Account 招待(owner 除く) |
member | 閲覧のみ(将来用、MVP では未使用) |
未決事項 / TODO
- パスワード認証の完全廃止:
account.password削除でリカバリ手段がパスキー再発行のみになる。マジックリンク(メール経由再認証)の併用要否を要検討 - 招待フロー: owner が他 Account を招待する API の設計(Phase E 以降)
- Account 課金プラン:
accountsOrg に紐づく課金エンティティの設計(別ドキュメント化予定)。stg-accounts配下は除外 - 監査ログ: Account の操作ログをどこに保存するか(既存 AccessToken 監査と統合するか別系統か)
- EcAuth 自身の管理者(superadmin): EcAuth 運営者が顧客 Account をサポート目的で代行操作する必要があるか。必要なら
account.role=superadmin等の特別フラグが必要 - stg-accounts のデータクリーンアップ運用: 自動削除スクリプトの設計(cron + Azure Functions 等)