EcAuth ドメインアーキテクチャ設計書
EcAuth が提供するドメイン構成、ゾーン分割、サブドメイン設計、Cloudflare 上のレコード戦略。
1. 概要
EcAuth は本番環境で独自ドメイン ec-auth.io
を使用し、サブドメインベースのマルチテナンシーを実現する。各
Organization(テナント)は {tenant}.ec-auth.io
の形式でアクセスされ、TenantMiddleware
がホスト名からテナントを自動的に識別する。
ドメイン取得・管理
| 項目 | 値 |
|---|---|
| ドメイン | ec-auth.io |
| レジストラ | Cloudflare |
| DNS 管理 | Cloudflare |
| SSL/TLS | Cloudflare Universal SSL(エッジ)+ Origin Certificate(オリジン) |
2. ドメイン設計
サブドメインマッピング
| サブドメイン | 用途 |
|---|---|
{tenant}.ec-auth.io |
テナント固有の IdP エンドポイント |
api.ec-auth.io |
テナント横断の共通 API
エンドポイント(/platform/*) |
ec-auth.io(ルートドメイン) |
ランディングページ / ドキュメント / リダイレクト |
例:
production.ec-auth.io→ 本番テナント「production」の IdPshopname.ec-auth.io→ 店舗「shopname」の IdPapi.ec-auth.io/platform/v1/client-resolve→ Client ID からテナントを解決
URL 構造
https://{tenant}.ec-auth.io/v1/authorization ← B2C 認可エンドポイント
https://{tenant}.ec-auth.io/v1/token ← トークンエンドポイント
https://{tenant}.ec-auth.io/v1/userinfo ← ユーザー情報エンドポイント
https://{tenant}.ec-auth.io/v1/b2b/passkey/* ← B2B パスキー API
https://{tenant}.ec-auth.io/.well-known/openid-configuration ← OIDC ディスカバリ
https://api.ec-auth.io/platform/v1/client-resolve ← Client ID → テナント解決(共通 API)
共通 API エンドポイント
(api.ec-auth.io)
テナントコンテキストに依存しないテナント横断 API を提供する(EcAuth#342 / EcAuthDocs#58 で導入)。
| 項目 | 値 |
|---|---|
| ホスト | api.ec-auth.io |
| パスプレフィックス | /platform/* |
| TenantMiddleware | スキップ(テナント解決を行わない) |
| ルーティング先 | Azure App Service の本番 IdP(*.ec-auth.io
と同じオリジン) |
| DNS | Cloudflare のワイルドカード CNAME *.ec-auth.io
が自動的にカバー(追加の CNAME は不要) |
| Azure カスタムドメイン検証 | asuid.api TXT レコードを別途追加 |
| SSL | 既存の *.ec-auth.io ワイルドカード Origin Certificate
を再利用 |
現在提供している /platform/* エンドポイント:
GET /platform/v1/client-resolve?client_id=xxx→{ tenant_name, base_url, organization_name }
3. ネットワークアーキテクチャ
flowchart LR
Browser["ブラウザ / EC-CUBE"]
CF["Cloudflare Edge
(SSL 終端, WAF, DDoS 保護)"]
Azure["Azure App Service
(Origin)"]
DB["Azure SQL Database"]
Browser -->|HTTPS| CF
CF -->|"HTTPS (Origin Certificate)"| Azure
Azure --> DB
通信フロー
- クライアント(ブラウザ / EC-CUBE プラグイン)が
https://{tenant}.ec-auth.ioにリクエスト - Cloudflare Edge で SSL 終端、WAF ルール適用、DDoS 保護
- Cloudflare が Azure App Service(オリジン)に Origin Certificate を使用して HTTPS 転送
- App Service 上の
TenantMiddlewareがホスト名からテナントを抽出 - レスポンスは同じ経路で返却
4. DNS アーキテクチャ
DNS レコード構成
| 種別 | 名前 | 値 | プロキシ | 用途 |
|---|---|---|---|---|
| CNAME | * | {web_app_name}.azurewebsites.net | Proxied | ワイルドカード(全テナント) |
| CNAME | @ | {web_app_name}.azurewebsites.net | Proxied | ルートドメイン |
| TXT | asuid |
{Azure 検証 ID} |
DNS only | Azure ドメイン検証 |
| TXT | asuid.* |
{Azure 検証 ID} |
DNS only | Azure ワイルドカードドメイン検証 |
Terraform Cloudflare Provider
# Cloudflare DNS レコード管理
resource "cloudflare_record" "wildcard" {
zone_id = var.cloudflare_zone_id
name = "*"
content = "ecauth-prod.azurewebsites.net"
type = "CNAME"
proxied = true
}
resource "cloudflare_record" "root" {
zone_id = var.cloudflare_zone_id
name = "@"
content = "ecauth-prod.azurewebsites.net"
type = "CNAME"
proxied = true
}
# Azure ドメイン検証用 TXT レコード
resource "cloudflare_record" "azure_verification" {
zone_id = var.cloudflare_zone_id
name = "asuid"
content = azurerm_linux_web_app.this.custom_domain_verification_id
type = "TXT"
proxied = false
}
5. SSL/TLS 戦略
構成
flowchart LR
subgraph Edge["Cloudflare Edge"]
UniversalSSL["Universal SSL
(*.ec-auth.io)"]
end
subgraph Origin["Azure App Service"]
OriginCert["Cloudflare Origin Certificate
(*.ec-auth.io, 15年有効)"]
end
Client -->|TLS 1.2+| UniversalSSL
UniversalSSL -->|"TLS (Origin Cert)"| OriginCert
SSL モード: Full (strict)
| レイヤー | 証明書 | 管理方法 |
|---|---|---|
| Edge(クライアント→Cloudflare) | Cloudflare Universal SSL | 自動発行・自動更新 |
| Origin(Cloudflare→Azure) | Cloudflare Origin Certificate | Cloudflare ダッシュボード / API で生成、Azure にインストール |
Origin Certificate の特徴
- Cloudflare が発行する専用証明書(公的 CA ではない)
- 最大 15 年有効
- ワイルドカード対応(
*.ec-auth.io) - Cloudflare プロキシ経由でのみ信頼される
6. Azure App Service カスタムドメイン
カスタムドメインバインディング
# ワイルドカードカスタムドメイン
resource "azurerm_app_service_custom_hostname_binding" "wildcard" {
hostname = "*.ec-auth.io"
app_service_name = azurerm_linux_web_app.this.name
resource_group_name = var.resource_group_name
}
# ルートドメイン
resource "azurerm_app_service_custom_hostname_binding" "root" {
hostname = "ec-auth.io"
app_service_name = azurerm_linux_web_app.this.name
resource_group_name = var.resource_group_name
}
App Service プラン要件
| プラン | カスタムドメイン | SSL バインディング | 備考 |
|---|---|---|---|
| F1 (Free) | 不可 | 不可 | - |
| B1 (Basic) | 対応 | 対応 | 本番環境で使用 |
| S1 (Standard) | 対応 | 対応 | - |
現在の本番環境は B1 プランを使用しており、カスタムドメインと SSL バインディングに対応しています。
注意: B1 プランのデプロイ時ダウンタイムについて
B1 プランにはデプロイメントスロット(ステージングスロット)がないため、デプロイ時にアプリケーションの再起動が発生する(数秒〜数十秒程度)。この間、新規ログインを試みたユーザーは認証エラーとなる。
- B2B パスキー: EC-CUBE プラグインからの API 呼び出しが 502/503 エラー
- B2C フェデレーション:
/v1/authorizationまたは/v1/authorization/callbackでエラーページ表示 - ログイン済みユーザー: EC-CUBE セッションは独立しているため影響なし
現段階(初期段階・手動デプロイ・低トラフィック)では許容可能。テナント数・トラフィック増加時には P0v4 プラン(Premium V4 最小構成、デプロイメントスロット対応)へのアップグレードを検討すること。
Origin Certificate のインストール
Cloudflare Origin Certificate を Azure App Service にインストールする方法:
- Cloudflare ダッシュボードで Origin Certificate
を生成(
*.ec-auth.io) - PFX 形式に変換して Azure App Service にアップロード
- カスタムドメインに SSL バインディングを設定
# Origin Certificate を App Service にアップロード
resource "azurerm_app_service_certificate" "origin" {
name = "cloudflare-origin-cert"
resource_group_name = var.resource_group_name
location = var.location
pfx_blob = var.origin_certificate_pfx_base64
password = var.origin_certificate_password
}
7. 環境別ドメインマッピング
| 環境 | ドメイン | テナント抽出方法 | DEFAULT_ORGANIZATION_TENANT_NAME |
|---|---|---|---|
| Dev(ローカル Docker) | localhost:8081 |
環境変数フォールバック | dev |
| Staging | ecauth-staging-{suffix}.azurewebsites.net |
環境変数フォールバック | staging |
| Production | {tenant}.ec-auth.io |
サブドメイン抽出 | production |
Dev / Staging と Production の違い
- Dev / Staging: Azure
プラットフォームドメイン(
*.azurewebsites.net)を使用。TenantMiddlewareはこれらのドメインを検出してDEFAULT_ORGANIZATION_TENANT_NAMEにフォールバックする。web_app_suffixが必要。 - Production:
カスタムドメイン(
*.ec-auth.io)を使用。TenantMiddlewareがサブドメインからテナント名を抽出する。web_app_suffixは不要。
8. TenantMiddleware の動作
TenantMiddleware
は既にサブドメインベースのテナント識別に対応しており、コード変更は不要。
抽出ロジック(TenantMiddleware.cs
L33-63)
private string ExtractTenantNameFromHost(string host)
{
// IPアドレスの場合 → 空文字列(デフォルトテナント)
if (IPAddress.TryParse(host, out _))
return string.Empty;
// localhost の場合 → 空文字列(デフォルトテナント)
if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
return string.Empty;
// Azure プラットフォームドメインの場合 → 空文字列(デフォルトテナント)
if (host.EndsWith(".azurewebsites.net", StringComparison.OrdinalIgnoreCase) ||
host.EndsWith(".azurecontainerapps.io", StringComparison.OrdinalIgnoreCase))
return string.Empty;
// サブドメイン形式からテナント名を抽出
var segments = host.Split('.');
if (segments.Length > 2)
return segments[0]; // ← テナント名
return string.Empty;
}テナント抽出の具体例
| ホスト名 | segments |
結果 |
|---|---|---|
production.ec-auth.io |
["production", "ec-auth", "io"] (3) |
"production" |
shopname.ec-auth.io |
["shopname", "ec-auth", "io"] (3) |
"shopname" |
localhost |
- | "" → 環境変数フォールバック |
ecauth-staging-xxx.azurewebsites.net |
- | "" → 環境変数フォールバック |
フォールバック動作(TenantMiddleware.cs
L19-28)
var tenantName = ExtractTenantNameFromHost(host);
var defaultOrganizationTenantName = Environment.GetEnvironmentVariable("DEFAULT_ORGANIZATION_TENANT_NAME") ?? string.Empty;
var finalTenantName = string.IsNullOrEmpty(tenantName) ? defaultOrganizationTenantName : tenantName;サブドメインが抽出できない場合(localhost、Azure ドメイン)は
DEFAULT_ORGANIZATION_TENANT_NAME
環境変数にフォールバックする。
9. 1Password アイテム設計
本番環境のドメインがカスタムドメインに変更されることに伴い、1Password
アイテムの設計を更新する。web_app_suffix
は本番環境では不要。
ecauth-prod-sql (DATABASE)
| フィールド | 値 | 備考 |
|---|---|---|
| hostname | ecauth-prod-sql.database.windows.net |
Azure SQL Database |
| database | EcAuthDb |
|
| username | ecauth_prod_admin |
|
| password | (ランダム生成 32文字) |
ecauth-prod-app (SECURE_NOTE)
| フィールド | 値 | 備考 |
|---|---|---|
| organization_code | production |
|
| organization_name | Production Environment |
|
| organization_tenant_name | production |
サブドメインと一致 |
| client_id | prod-client-id |
|
| client_secret | (ランダム生成) | |
| app_name | production-app |
|
| redirect_uri | https://production.ec-auth.io/v1/auth/callback |
カスタムドメイン使用 |
| state_password | (ランダム生成) | |
| b2b_user_subject | (UUID 生成) | |
| b2b_user_external_id | prod-admin |
|
| b2b_redirect_uri | https://production.ec-auth.io/admin/ecauth/callback |
カスタムドメイン使用 |
| b2b_allowed_rp_ids | production.ec-auth.io,localhost |
カスタムドメイン使用 |
ステージングとの違い:
web_app_suffixフィールドなし(カスタムドメインのため不要)- URL が
*.azurewebsites.net→*.ec-auth.io - Web App 名(
ecauth-prod)は Terraform 変数var.web_app_nameで管理
ecauth-prod-mockidp (SECURE_NOTE)
| フィールド | 値 | 備考 |
|---|---|---|
| app_name | production-mockidp |
|
| client_id | production-mockclientid |
|
| client_secret | (ランダム生成) |
mockidp-production (PASSWORD)
| フィールド | 値 | 備考 |
|---|---|---|
| base_url | https://mock-openid-provider.mangoplant-f8a75293.japaneast.azurecontainerapps.io |
既存 Container App 共有 |
| authorization_endpoint | {base_url}/authorization?org=production |
?org=production パラメータ |
| token_endpoint | {base_url}/token?org=production |
|
| userinfo_endpoint | {base_url}/userinfo?org=production |
|
| default_user_email | production-user@example.com |
|
| default_user_password | (ランダム生成) |
Terraform からの参照方法
# ecauth-prod-app の参照(web_app_suffix を使わない)
locals {
op_prod_app_fields = {
for f in data.onepassword_item.prod_app.section[0].field : f.label => f.value
}
# web_app_suffix は不要。Web App 名は var.web_app_name で直接指定
# web_app_suffix = local.op_prod_app_fields["web_app_suffix"] ← 削除
client_id = local.op_prod_app_fields["client_id"]
redirect_uri = local.op_prod_app_fields["redirect_uri"]
# ...
}
module "web_app_identityprovider" {
# web_app_name を直接指定(1Password に依存しない)
web_app_name = var.web_app_name # デフォルト: "ecauth-prod"
}
10. GitHub Actions ワークフローへの影響
現在の問題
deploy-production.yml と
migrate-production.yml が WEB_APP_SUFFIX
GitHub Secret を参照して URL を組み立てている。
# 現在の deploy-production.yml (L78, L83)
app-name: ecauth-prod-${{ secrets.WEB_APP_SUFFIX }}
HEALTH_CHECK_URL: https://ecauth-prod-${{ secrets.WEB_APP_SUFFIX }}.azurewebsites.net/healthz
# 現在の migrate-production.yml (L150)
ECAUTH_BASE_URL: https://ecauth-prod-${{ secrets.WEB_APP_SUFFIX }}.azurewebsites.net変更後
# deploy-production.yml
app-name: ecauth-prod # 固定値
HEALTH_CHECK_URL: https://production.ec-auth.io/healthz # カスタムドメイン
# verify ジョブの URL もカスタムドメインに変更
ECAUTH_BASE_URL: https://production.ec-auth.io
E2E_BASE_URL: https://production.ec-auth.io
DEV_B2B_ALLOWED_RP_IDS: production.ec-auth.io # 1Password の値と一致
# migrate-production.yml
ECAUTH_BASE_URL: https://production.ec-auth.io不要になる GitHub Secret
| Secret | 状態 |
|---|---|
WEB_APP_SUFFIX |
production 環境から削除可能 |
11. Terraform への影響
現在の問題(production/main.tf)
# L44: web_app_suffix を 1Password から取得
web_app_suffix = local.op_prod_app_fields["web_app_suffix"]
# L145: web_app_suffix にフォールバック
web_app_name = var.web_app_name != "" ? var.web_app_name : "ecauth-prod-${local.web_app_suffix}"
変更内容
web_app_suffix依存の除去:var.web_app_nameにデフォルト値"ecauth-prod"を設定し、1Password のweb_app_suffixへの依存を削除- Cloudflare Provider 追加: DNS レコード、Origin Certificate の管理
- カスタムドメインバインディング追加:
azurerm_app_service_custom_hostname_binding - Azure IP 制限: Cloudflare IP のみ許可(オリジン保護)
変更後のイメージ
# variables.tf
variable "web_app_name" {
description = "Web App name"
type = string
default = "ecauth-prod" # デフォルト値を設定
}
# main.tf
module "web_app_identityprovider" {
web_app_name = var.web_app_name # フォールバック不要
}
# Cloudflare DNS
resource "cloudflare_record" "wildcard" {
zone_id = var.cloudflare_zone_id
name = "*"
content = "${var.web_app_name}.azurewebsites.net"
type = "CNAME"
proxied = true
}
# カスタムドメインバインディング
resource "azurerm_app_service_custom_hostname_binding" "wildcard" {
hostname = "*.ec-auth.io"
app_service_name = module.web_app_identityprovider.web_app_name
resource_group_name = data.azurerm_resource_group.this.name
}
12. セキュリティ考慮事項
Cloudflare WAF / DDoS 保護
- Cloudflare プロキシを有効化(Proxied モード)することで、オリジン IP を秘匿
- Cloudflare WAF ルールによる一般的な攻撃パターンの防御
- DDoS 保護はプラン共通で提供
オリジン IP 保護
Azure App Service のアクセス制限で、Cloudflare IP のみを許可する:
# Cloudflare IP レンジを動的に取得
data "cloudflare_ip_ranges" "this" {}
resource "azurerm_linux_web_app" "this" {
site_config {
dynamic "ip_restriction" {
for_each = data.cloudflare_ip_ranges.this.ipv4_cidr_blocks
content {
name = "cloudflare-ipv4-${ip_restriction.key}"
ip_address = ip_restriction.value
action = "Allow"
priority = 100 + ip_restriction.key
}
}
ip_restriction_default_action = "Deny"
}
}
HTTPS 強制
- Cloudflare 側: 「Always Use HTTPS」を有効化
- Azure 側:
HTTPS Only = true(既に設定済み)
HSTS
Cloudflare 側で HSTS ヘッダーを設定:
max-age: 31536000(1年)includeSubDomains: truepreload: true