コンテンツにスキップ

PII分離アーキテクチャ

Authrimは、個人識別情報(PII)と非PIIデータをデータベースレベルとアプリケーションレベルの両方で厳密に分離しています。このアーキテクチャにより、認証フローの高パフォーマンスを維持しながら、プライバシー規制(GDPR、CCPA、個人情報保護法)への準拠を実現します。

なぜ物理的にDBを分離するのか

テーブルレベルでの分離を使用する従来のアプローチとは異なり、Authrimは PIIを専用データベースに物理的に分離します:

  1. 監査・コンプライアンス: 「PIIは別DBに格納」はGDPR/CCPAの監査で「テーブルレベルで分離」より説得力がある
  2. リージョン展開: EU専用PII DBの追加は新しいD1バインディングを追加するだけ、スキーマ変更不要
  3. アクセス制御の強制: PIIContextを持たないコードはPIIにアクセスできない - コードレビューで一目瞭然

アーキテクチャ概要

flowchart TB
    subgraph APP["アプリケーション層 (Honoハンドラー, サービス, フロー)"]
        A1["/authorize
Core+Cacheのみ"] A2["/token
Core+Cacheのみ"] A3["/userinfo
Core+Cache+PII"] end subgraph REPO["リポジトリ層"] R1["CoreRepository
• UserCore
• Passkey
• Session
• Role
• Relationship
• OAuthClient"] R2["PIIRepository
• UserProfile
• Identifiers
• LinkedIdentity
• AuditLog(PII)"] R3["CacheRepository
• UserCache
• ConsentCache
• RBACCache
• ClientCache"] end subgraph ADAPTER["データベースアダプター層"] AD1["D1Adapter"] AD2["DOAdapter"] AD3["KVAdapter"] AD4["PGAdapter
(Regional)"] end subgraph STORAGE["ストレージ"] DB1["グローバル非PII (D1/DO)
• users_core
• passkeys
• sessions
• roles
• relationships
• oauth_clients"] DB2["グローバルキャッシュ (KV/DO)
• USER_CACHE
• REBAC_CACHE
• CONSENT_CACHE
• CLIENTS_CACHE"] DB3["リージョナルPII (D1/Postgres)
EU: users_pii
JP: users_pii
US: users_pii
• identifiers
• linked_ids"] end APP --> REPO REPO --> ADAPTER AD1 --> DB1 AD2 --> DB1 AD3 --> DB2 AD4 --> DB3

三層リポジトリパターン

リポジトリは異なる特性を持つ3層に分割されます:

特性用途
CacheRepository最速・揮発性・安価UserInfo高速化、RBACクレームキャッシュ
CoreRepository耐久性・整合性・グローバル認証・認可の真実のソース
PIIRepository地域縛り・GDPR対応・暗号化個人データのみ

TypeScriptレベルでのアクセス制御

TypeScriptの型システムがコンパイル時にPII境界を強制します。

コンテキスト型

// PIIにアクセスできないContext(/authorize, /token用)
interface AuthContext {
core: CoreRepository;
cache: CacheRepository;
// piiプロパティなし → PIIにアクセス不可能
}
// PIIにアクセスできるContext(/userinfo, /admin/users用)
interface PIIContext extends AuthContext {
pii: PIIRepository;
}
// ハンドラー型定義
type AuthHandler = (c: HonoContext, ctx: AuthContext) => Promise<Response>;
type PIIHandler = (c: HonoContext, ctx: PIIContext) => Promise<Response>;

コンパイル時の強制

// authorize.ts - AuthHandler型なのでctx.piiアクセスはコンパイルエラー
export const authorizeHandler: AuthHandler = async (c, ctx) => {
const client = await ctx.cache.getCachedClient(clientId); // ✅ OK
const session = await ctx.core.getSession(sessionId); // ✅ OK
// ❌ コンパイルエラー: プロパティ 'pii' は型 'AuthContext' に存在しません
// const profile = await ctx.pii.getUserProfile(userId);
return c.redirect(redirectUri);
};
// userinfo.ts - PIIHandler型なのでctx.piiにアクセス可能
export const userinfoHandler: PIIHandler = async (c, ctx) => {
const core = await ctx.core.getUserCore(userId); // ✅ OK
const profile = await ctx.pii.getUserProfile(userId); // ✅ OK
return c.json({ sub: core.id, email: profile.email });
};

PII分類

PIIを含むテーブル(分離が必要)

テーブルPIIフィールドリスクレベル
users_piiemail, name, phone_number, address, birthdateCritical
user_custom_fieldsfield_value(任意のPIIが入る可能性)High
subject_identifiersidentifier_value(email, phone, DID)High
linked_identitiesprovider_email, raw_claims, profile_dataHigh
audit_log_piiip_address, user_agentMedium

非PIIテーブル(分離不要)

カテゴリテーブル説明
認証インフラpasskeys, sessions, password_reset_tokens公開鍵、セッションID(UUID参照)
認可/RBACroles, user_roles, relationships, organizationsロール定義、UUID参照のみ
設定oauth_clients, upstream_providers, scope_mappingsクライアント設定、IdP設定

トークンのPII分析

Authrimのトークンは PII を含まないように設計されています:

Access Token

const accessTokenClaims = {
iss: env.ISSUER_URL, // ✅ 非PII
sub: authCodeData.sub, // ✅ UUID(非PII)
scope: authCodeData.scope, // ✅ 非PII
authrim_roles: [...], // ✅ 非PII
authrim_org_id: "...", // ✅ UUID(非PII)
};

結論: Access TokenにPIIは含まれない。subはUUID、email/nameはUserInfoエンドポイント経由でのみ取得。

データベーススキーマ

コアデータベース(グローバルD1)

CREATE TABLE users_core (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
pii_partition TEXT NOT NULL DEFAULT 'default',
pii_status TEXT NOT NULL DEFAULT 'pending', -- pending/active/failed/none/deleted
email_verified INTEGER DEFAULT 0,
user_type TEXT NOT NULL DEFAULT 'end_user',
user_version INTEGER NOT NULL DEFAULT 1,
is_deleted INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);

PIIデータベース(リージョナル)

CREATE TABLE users_pii (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL DEFAULT 'default',
email_encrypted TEXT NOT NULL,
email_blind_index TEXT NOT NULL, -- 検索可能暗号化用
name TEXT,
phone_number_encrypted TEXT,
address_encrypted TEXT,
pii_class TEXT NOT NULL DEFAULT 'IDENTITY_CORE',
anonymized_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_users_pii_email
ON users_pii(tenant_id, email_blind_index);

Blind Index(検索可能暗号化)

暗号化されたPIIフィールドを検索可能にするため:

function createBlindIndex(value: string, masterIndexKey: string): string {
const normalized = value.toLowerCase().trim();
return crypto.createHmac('sha256', masterIndexKey)
.update(normalized)
.digest('base64url');
}
// 使用例: emailをインデックスに露出せずにユーザーを検索
async function findUserByEmail(email: string): Promise<string | null> {
const blindIndex = createBlindIndex(email, env.INDEX_KEY);
const result = await piiDb.query(
'SELECT id FROM users_pii WHERE email_blind_index = ?',
[blindIndex]
);
return result?.id ?? null;
}

リージョン/パーティションルーティング

type Partition = 'default' | 'eu' | 'jp' | 'us' | string;
class PartitionRouter {
async getPIIAdapter(userId: string): Promise<DatabaseAdapter> {
// Core DBからユーザーのパーティションを取得
const user = await coreDb.queryOne<{ pii_partition: Partition }>(
'SELECT pii_partition FROM users_core WHERE id = ?',
[userId]
);
return this.getAdapterForPartition(user?.pii_partition || 'default');
}
getAdapterForPartition(partition: Partition): DatabaseAdapter {
// 適切なリージョナルデータベースアダプターを返す
switch (partition) {
case 'eu': return new D1Adapter(env.DB_PII_EU);
case 'jp': return new D1Adapter(env.DB_PII_JP);
default: return new D1Adapter(env.DB_PII);
}
}
}

マルチテナント分離レベル

Authrimは3段階のテナント分離をサポート:

レベル説明ユースケース
Row-levelWHERE tenant_id = ?デフォルト、最もコスト効率が良い
Schema-leveltenant_abc.users_piiエンタープライズ、個別バックアップ
Database-level専用DBインスタンス最大分離、リージョナルコンプライアンス
class PIIRepository {
static forTenant(tenantId: string, config: TenantConfig): PIIRepository {
switch (config.isolation_level) {
case 'database':
return new PIIRepository(getDedicatedAdapter(tenantId));
case 'schema':
return new PIIRepository(sharedDb, `tenant_${tenantId}`);
default:
return new PIIRepository(defaultDb, 'public', tenantId);
}
}
}

Circuit BreakerとGraceful Degradation

リージョナルPII DBが障害を起こしても認証フローは継続:

export const userinfoHandler: PIIHandler = async (c, ctx) => {
// Coreは必須
const core = await ctx.core.getUserCore(userId);
if (!core) return c.json({ error: 'invalid_token' }, 401);
// PIIはCircuit Breaker経由
const profile = await piiCircuitBreaker.execute(
() => ctx.pii.getUserProfile(userId),
() => Promise.resolve(null) // フォールバック: nullを返す
);
// PIIが利用不可の場合はdegradedレスポンス
return c.json({
sub: core.id,
email: profile?.email ?? null,
name: profile?.name ?? null,
_degraded: profile === null, // クライアントにdegraded状態を通知
});
};

GDPRコンプライアンス

Soft Delete + 匿名化

async function deleteUser(userId: string, mode: 'hard_delete' | 'anonymize') {
// 1. キャッシュを即時無効化
await cache.invalidateUser(userId);
// 2. PIIを処理
if (mode === 'hard_delete') {
await pii.deleteUserProfile(userId);
} else {
await pii.anonymizeUserProfile(userId);
// email → "deleted_{userId}@anonymized.local"
// name, phone, address → NULL
}
// 3. Coreでsoft delete(監査用に保持)
await core.updateUserCore(userId, {
is_deleted: true,
deleted_at: Date.now()
});
// 4. relationships, sessions, passkeysを削除
await core.deleteUserRelationships(userId);
await core.deleteUserSessions(userId);
}

Tombstoneテーブル

CREATE TABLE users_pii_tombstone (
user_id TEXT PRIMARY KEY,
email_blind_index TEXT, -- 再登録防止
deleted_by TEXT NOT NULL,
deletion_reason TEXT,
deleted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
retention_until TIMESTAMPTZ NOT NULL -- パージするタイミング
);

メリット

1. 規制コンプライアンス

  • GDPR: DSARと消去権の容易な実装
  • CCPA: 「販売禁止」要件への明確な分離
  • データレジデンシー: PIIデータベースを特定リージョンにデプロイ可能

2. セキュリティ

  • 攻撃対象の縮小: コア認証サービスはPIIにアクセスできない
  • 最小権限の原則: PII専用エンドポイントのみが個人データにアクセス
  • 監査証跡: すべてのPIIアクセスを個別にログ記録

3. パフォーマンス

  • 小さいコアデータベース: 認証/認可のクエリが高速
  • 安全なキャッシング: コアデータはグローバルに積極的にキャッシュ可能
  • トークンにPIIなし: トークンはコンパクトでセキュア

ベストプラクティス

  1. データベース間でJOINしない - Promise.allを使用して各データベースから個別にフェッチ
  2. ユーザーIDをリンクとして使用 - データベース間で共有される唯一のフィールド
  3. すべてのPIIアクセスをログ記録 - すべてのPII操作に監査ログを実装
  4. pii_status状態機械を使用 - PII書き込み状態を追跡(pending/active/failed/none)
  5. regionよりpartitionを優先 - より柔軟なルーティング(テナント、プラン、セキュリティレベル)
  6. IPルーティングはデフォルトOFF - VPN/proxyによりIPはコンプライアンスで信頼できない