# 機能設計書 9-メンバー認証

## 概要

本ドキュメントは、Ghost CMSにおけるメンバー認証機能について、その設計仕様を記載する。メンバー認証機能は、サイト購読者（メンバー）のログイン・ログアウトとセッション管理を担う。

### 本機能の処理概要

メンバー認証機能は、Ghost CMSサイトの購読者（メンバー）が保護されたコンテンツにアクセスするための認証を提供する。メンバー認証はマジックリンク（メールによるワンタイムトークン）方式を採用しており、パスワードレスでセキュアな認証を実現する。認証成功後はセッションCookieでセッション状態を維持する。

**業務上の目的・背景**：会員制コンテンツを提供するパブリッシャーにとって、メンバーの認証は必須機能である。パスワード管理の負担を軽減しつつセキュアな認証を実現するため、マジックリンク方式を採用している。

**機能の利用シーン**：
- 読者がPortal経由でメールアドレスを入力してログインリンクを受信する
- 読者がメール内のマジックリンクをクリックしてログインする
- 有料コンテンツにアクセスするためにメンバー認証が行われる
- メンバーがログアウトしてセッションを終了する

**主要な処理内容**：
1. マジックリンク送信（POST /members/api/send-magic-link/）
2. マジックリンクによるセッション作成（GET /?token=xxx）
3. セッションからのメンバー情報取得
4. ログアウト（セッション削除）
5. 認証トークン（JWT）の発行と検証

**関連システム・外部連携**：
- メールサービス（マジックリンク送信）
- JWT（認証トークン生成・検証）
- GeoIPサービス（ログイン時の位置情報取得）

**権限による制御**：
- メンバー認証はフロントエンド向け機能であり、Admin権限は不要
- 認証済みメンバーのみが保護されたコンテンツにアクセス可能

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 15 | Portalウィジェット | 主機能 | ログインフォーム・マジックリンク送信 |
| 16 | 会員ページ | 主機能 | ログイン状態の確認・ログアウト |

## 機能種別

認証・認可

## 入力仕様

### 入力パラメータ

#### マジックリンク送信

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| email | string | Yes | メールアドレス | isEmail形式 |
| emailType | string | No | メールタイプ（signin/subscribe/updateEmail） | 指定値のみ |
| redirect | string | No | ログイン後のリダイレクト先URL | - |

#### トークン交換（ログイン）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| token | string | Yes | マジックリンクに含まれるJWT | JWT形式 |
| otc_verification | string | No | ワンタイムコード検証用パラメータ | - |

### 入力データソース

- Portal（フロントエンドログインフォーム）
- メール内のマジックリンク
- セッションCookie（members-ssr）

## 出力仕様

### 出力データ

#### メンバーセッション情報

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | メンバーID |
| uuid | string | メンバーUUID |
| email | string | メールアドレス |
| name | string | 氏名 |
| transient_id | string | 一時的ID（セッション識別用） |
| status | string | ステータス（free/paid/comped） |
| products | array | 所属ティア配列 |
| subscriptions | array | サブスクリプション配列 |

#### 認証トークン（JWT）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| sub | string | メンバーのメールアドレス |
| aud | string | サイトのオリジン |
| iss | string | 発行者（Members APIのURL） |
| iat | number | 発行日時（Unix timestamp） |
| exp | number | 有効期限（Unix timestamp） |

### 出力先

- セッションCookie（members-ssr）
- JWT（Authorizationヘッダー）

## 処理フロー

### 処理シーケンス

```
【マジックリンクによるログイン】
1. メールアドレス入力（Portal）
   └─ send-magic-link APIリクエスト
2. マジックリンクメール送信
   ├─ JWT生成（メールアドレス埋め込み）
   └─ メールサービスで送信
3. マジックリンククリック
   └─ tokenパラメータ付きでサイトにアクセス
4. トークン検証・セッション作成
   ├─ JWT検証（署名、有効期限）
   ├─ メンバーデータ取得
   ├─ GeoIP位置情報設定（初回ログイン時）
   └─ セッションCookie設定（transient_id）
5. ログイン完了
   └─ リダイレクト（指定URLまたはホームページ）

【セッション確認】
1. リクエスト受信
   └─ members-ssr Cookie読み取り
2. transient_idからメンバー取得
   └─ Members APIでメンバー情報取得
3. メンバー情報返却

【ログアウト】
1. ログアウトリクエスト
   └─ セッションCookie削除
2. （全セッション削除時）transient_idサイクル
```

### フローチャート

```mermaid
flowchart TD
    A[ログインリクエスト] --> B[メールアドレス入力]
    B --> C[マジックリンク送信]
    C --> D[メール受信]
    D --> E[マジックリンククリック]
    E --> F{トークン検証}
    F -->|失敗| G[エラー表示]
    F -->|成功| H[メンバー取得]
    H --> I{初回ログイン?}
    I -->|Yes| J[GeoIP設定]
    I -->|No| K[セッションCookie設定]
    J --> K
    K --> L[リダイレクト]

    M[セッション確認] --> N{Cookie存在?}
    N -->|No| O[未認証状態]
    N -->|Yes| P[transient_id取得]
    P --> Q[メンバーデータ取得]
    Q --> R[メンバー情報返却]

    S[ログアウト] --> T{全セッション?}
    T -->|Yes| U[transient_idサイクル]
    T -->|No| V[Cookie削除]
    U --> V
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | マジックリンク有効期限 | マジックリンクには有効期限が設定される（JWT exp） | マジックリンク生成時 |
| BR-002 | セッション有効期間 | セッションCookieの有効期間は約6ヶ月（184日） | セッション作成時 |
| BR-003 | transient_id方式 | セッション識別にtransient_idを使用（全セッション無効化に対応） | 全認証時 |
| BR-004 | GhostMembers認証スキーム | JWTはAuthorizationヘッダーで「GhostMembers {token}」形式 | API認証時 |
| BR-005 | CSRF保護 | セッションCookieはSameSite=Lax、HttpOnly | Cookie設定時 |
| BR-006 | 全セッションログアウト | body.all=trueでtransient_idをサイクルして全セッション無効化 | ログアウト時 |

### 計算ロジック

- **セッション有効期間**: 1000 * 60 * 60 * 24 * 184 = 約6ヶ月
- **JWT署名アルゴリズム**: RS512（RSA-SHA512）

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ログイン時 | members | UPDATE | geolocationの更新（初回のみ） |
| ログアウト時 | members | UPDATE | transient_idの更新（全セッション無効化時） |
| セッション確認 | members | SELECT | transient_idでメンバー取得 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 400 | BadRequestError | トークンが無効または期限切れ | 再度マジックリンクを送信 |
| 400 | BadRequestError | セッションCookieが見つからない | 再度ログイン |
| 401 | UnauthorizedError | JWT検証失敗 | 再度ログイン |

### リトライ仕様

マジックリンクは何度でも再送信可能。トークン期限切れの場合は新しいマジックリンクを送信する。

## トランザクション仕様

特別なトランザクション制御は不要。GeoIP更新は非同期で行われ、失敗しても認証処理は継続する。

## パフォーマンス要件

- セッションCookie検証: リクエストごとに実行されるため高速である必要がある
- JWT検証: RS512署名検証のためCPUコストがかかる

## セキュリティ考慮事項

- **マジックリンク**: 一度使用されるとトークンは無効化される（JWT expによる自然失効）
- **Cookie属性**: HttpOnly=true, SameSite=Lax, Secure（本番環境）
- **transient_id方式**: 全セッション無効化に対応（パスワード変更相当の操作時）
- **署名検証**: RS512アルゴリズムによる堅牢な署名検証
- **CSRF保護**: SameSite=Laxによる基本的なCSRF保護

## 備考

- メンバー認証はAdmin認証とは完全に分離されたシステム
- 認証トークン（JWT）はAPI呼び出し用で、セッションCookieはSSR用
- ghost-access, ghost-access-hmac Cookieはキャッシュ最適化用

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: メンバー認証ミドルウェアを理解する

JWT認証ミドルウェアの実装を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | index.js | `ghost/core/core/server/services/auth/members/index.js` | メンバー認証ミドルウェア |

**主要処理フロー**:
- **8-35行目**: createMiddleware() - express-jwt設定
- **14行目**: credentialsRequired: false（オプショナル認証）
- **17行目**: audience: サイトオリジン
- **18行目**: issuer: Members API URL
- **19行目**: algorithms: ['RS512']
- **20行目**: secret: 公開鍵
- **21-33行目**: getToken() - 「GhostMembers {token}」形式からトークン抽出
- **38-56行目**: authenticateMembersToken getter - ミドルウェアファクトリ

**読解のコツ**: Authorizationヘッダーの「GhostMembers」スキームに注目。これがメンバー認証の識別子となる。

#### Step 2: SSRセッション管理を理解する

Server-Side Renderingでのセッション管理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | members-ssr.js | `ghost/core/core/server/services/members/members-ssr.js` | SSRセッション管理クラス |

**主要処理フロー**:
- **26行目**: SIX_MONTHS_MS = 約6ヶ月（セッション有効期間）
- **46-91行目**: constructor - Cookie設定初期化
- **72-78行目**: sessionCookieOptions（signed, httpOnly, sameSite: 'lax'）
- **111-119行目**: _removeSessionCookie() - Cookie削除（ghost-access系も削除）
- **128-134行目**: _setSessionCookie() - Cookie設定
- **144-158行目**: _getSessionCookies() - Cookie取得（署名検証付き）
- **168-171行目**: _getMemberDataFromToken() - JWTからメンバー取得
- **228-267行目**: exchangeTokenForSession() - トークン交換メイン処理
- **244行目**: マジックリンクトークンからメンバー取得
- **254-262行目**: GeoIP位置情報設定（初回ログイン時）
- **264行目**: transient_idでセッションCookie設定
- **281-290行目**: deleteSession() - ログアウト（all=trueで全セッション無効化）
- **300-304行目**: getMemberDataFromSession() - セッションからメンバー取得
- **314-324行目**: getIdentityTokenForMemberFromSession() - 認証トークン取得

#### Step 3: 管理者認証との比較（参考）

Admin認証のセッション管理と比較する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | session.js | `ghost/core/core/server/api/endpoints/session.js` | Admin セッションAPI |
| 3-2 | session-service.js | `ghost/core/core/server/services/auth/session/session-service.js` | Admin セッションサービス |
| 3-3 | middleware.js | `ghost/core/core/server/services/auth/session/middleware.js` | Admin セッションミドルウェア |

**session.js（Admin認証）**:
- **13-63行目**: add() - ログイン処理（username/password認証）
- **64-68行目**: delete() - ログアウト
- **69-78行目**: sendVerification/verify() - 2FA検証

**session-service.js（Admin認証）**:
- **99-101行目**: isVerificationRequired() - 2FA必要性判定
- **111-136行目**: createSessionForUser() - セッション作成
- **158-162行目**: generateAuthCodeForUser() - OTP生成
- **171-175行目**: verifyAuthCodeForUser() - OTP検証
- **245-289行目**: sendAuthCodeToUser() - 認証コードメール送信

**middleware.js（Admin認証）**:
- **4-27行目**: createSession() - セッション作成（2FA対応）
- **29-40行目**: logout() - ログアウト
- **42-59行目**: authenticate() - 認証チェック（verified状態確認）
- **71-84行目**: verifyAuthCode() - 認証コード検証

**メンバー認証との違い**:
- Admin: パスワード認証 + 2FA（OTP）
- メンバー: マジックリンク認証（パスワードレス）

### プログラム呼び出し階層図

```
【メンバー認証（フロントエンド向け）】

Portal / Frontend
    │
    ├─ マジックリンク送信
    │      └─ POST /members/api/send-magic-link/
    │             └─ Members API
    │                    └─ メール送信（JWT含む）
    │
    ├─ マジックリンククリック
    │      └─ GET /?token=xxx
    │             └─ MembersSSR.exchangeTokenForSession()
    │                    ├─ _getMemberDataFromToken()
    │                    │      └─ Members API
    │                    │             └─ JWT検証 + メンバー取得
    │                    ├─ _setMemberGeolocationFromIp()
    │                    └─ _setSessionCookie()
    │                           └─ transient_id をCookieに設定
    │
    ├─ セッション確認
    │      └─ MembersSSR.getMemberDataFromSession()
    │             ├─ _getSessionCookies()
    │             │      └─ members-ssr Cookie取得
    │             └─ _getMemberIdentityDataFromTransientId()
    │                    └─ Members API
    │                           └─ transient_idでメンバー取得
    │
    └─ API認証（コンテンツアクセス）
           └─ Authorization: GhostMembers {token}
                  └─ authenticateMembersToken middleware
                         └─ express-jwt
                                └─ JWT検証（RS512）

【Admin認証（管理画面向け）】

Ghost Admin
    │
    ├─ ログイン
    │      └─ POST /ghost/api/admin/session/
    │             └─ session.add()
    │                    ├─ User.check() パスワード検証
    │                    └─ sessionService.createSessionForUser()
    │
    ├─ 2FA検証
    │      └─ POST /ghost/api/admin/session/verify/
    │             └─ session.verify()
    │                    └─ sessionService.verifyAuthCodeForUser()
    │
    └─ ログアウト
           └─ DELETE /ghost/api/admin/session/
                  └─ session.delete()
                         └─ sessionService.removeUserForSession()
```

### データフロー図

```
[入力]                    [処理]                           [出力]

【マジックリンクログイン】

Portal (Email Input)
       │
       ▼
┌─────────────────┐
│ POST            │
│ /send-magic-link│
└────────┬────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ Members API     │───▶│ Email Service   │
│ JWT生成         │    │ マジックリンク  │
│ (email埋込)     │    │ メール送信      │
└─────────────────┘    └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ User clicks     │
                       │ magic link      │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ GET /?token=xxx │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ MembersSSR      │
                       │ exchangeToken   │
                       │ ForSession()    │
                       └────────┬────────┘
                                │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       ▼
           ┌─────────────────┐     ┌─────────────────┐
           │ JWT検証         │     │ GeoIP設定       │
           │ メンバー取得    │     │ (初回のみ)      │
           └────────┬────────┘     └────────┬────────┘
                    │                       │
                    └───────────┬───────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Set-Cookie      │
                       │ members-ssr=    │
                       │ {transient_id}  │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Redirect to     │
                       │ requested page  │
                       └─────────────────┘

【セッション確認】

Browser Request
       │
       ▼
┌─────────────────┐
│ Cookie:         │
│ members-ssr=xxx │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ MembersSSR      │
│ getMemberData   │
│ FromSession()   │
└────────┬────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ Cookie署名検証  │───▶│ transient_idで  │
│ transient_id取得│    │ メンバー検索    │
└─────────────────┘    └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ Member Data     │
                       │ JSON Response   │
                       └─────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.js | `ghost/core/core/server/services/auth/members/index.js` | ミドルウェア | メンバーJWT認証 |
| members-ssr.js | `ghost/core/core/server/services/members/members-ssr.js` | サービス | SSRセッション管理 |
| session.js | `ghost/core/core/server/api/endpoints/session.js` | API | Admin セッションAPI |
| session-service.js | `ghost/core/core/server/services/auth/session/session-service.js` | サービス | Admin セッションサービス |
| middleware.js | `ghost/core/core/server/services/auth/session/middleware.js` | ミドルウェア | Admin セッションミドルウェア |
| index.js | `ghost/core/core/server/services/auth/session/index.js` | エントリポイント | Admin セッション初期化 |
| express-session.js | `ghost/core/core/server/services/auth/session/express-session.js` | セッション | Expressセッション設定 |
