# 機能設計書 2-サインイン（マジックリンク）

## 概要

本ドキュメントは、Fizzyアプリケーションにおけるサインイン機能の設計書である。メールアドレスを入力するとマジックリンク（6桁コード）がメールで送信され、そのコードを入力してログインするパスワードレス認証方式を採用している。

### 本機能の処理概要

**業務上の目的・背景**：パスワード管理の煩雑さを排除し、ユーザーの認証体験を簡素化するためにマジックリンク認証を採用している。ユーザーはパスワードを覚える必要がなく、メールアドレスのみで安全にサインインできる。また、パスワード漏洩のリスクがなく、フィッシング攻撃にも強い認証方式となっている。

**機能の利用シーン**：既存ユーザーがFizzyにログインする際に使用する。ログイン画面でメールアドレスを入力し、メールで届くマジックリンクコードを入力してセッションを開始する。新規ユーザーの場合はサインアップ処理に自動的に切り替わる。

**主要な処理内容**：
1. ログイン画面でメールアドレスを入力
2. 既存ユーザーの場合：Identity検索とマジックリンク送信
3. 新規ユーザーの場合：サインアップ処理へ自動切り替え（テナント許可時）
4. 存在しないメールアドレスでテナント制限時：フェイクリンク画面表示（タイミング攻撃防止）
5. マジックリンクコード入力画面でコード検証
6. 検証成功時にセッション開始とCookie設定
7. アカウント選択またはダッシュボードへリダイレクト

**関連システム・外部連携**：メール送信にはRailsのAction Mailerを使用し、非同期処理（Active Job）で送信される。

**権限による制御**：サインインは未認証ユーザーのみが実行可能。既に認証済みのユーザーがアクセスした場合は、ルート画面にリダイレクトされる。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 1 | ログイン画面 | 主画面 | メールアドレス入力とマジックリンク送信処理 |
| 4 | マジックリンク送信完了画面 | 結果表示画面 | マジックリンク送信後の確認画面表示 |
| 5 | セッション転送画面 | 参照画面 | セッション転送処理の実行 |

## 機能種別

認証処理 / セッション管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| email_address | String | Yes | ユーザーのメールアドレス | 存在チェック（Identity検索） |
| code | String | Yes | マジックリンクコード | 6桁の英数字、有効期限内 |

### 入力データソース

- 画面入力（ログインフォーム）
- 画面入力（マジックリンクコード入力フォーム）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| magic_link | MagicLink | 生成されたマジックリンクレコード |
| session | Session | 作成されたセッションレコード |
| session_token | String | Cookieに保存されるセッショントークン |

### 出力先

- データベース（magic_links, sessionsテーブル）
- メール送信（マジックリンクコードを含むメール）
- Cookie（セッショントークン）

## 処理フロー

### 処理シーケンス

```
1. ログイン画面表示（SessionsController#new）
   └─ メールアドレス入力フォームを表示
2. メールアドレス送信（SessionsController#create）
   └─ Identity.find_by(email_address:)で既存ユーザー検索
   └─ 既存: sign_in → マジックリンク送信
   └─ 新規＋テナント許可: sign_up → サインアップ処理
   └─ 新規＋テナント制限: フェイクリンク画面へリダイレクト
3. マジックリンク画面表示（Sessions::MagicLinksController#show）
   └─ コード入力フォームを表示
4. マジックリンク検証（Sessions::MagicLinksController#create）
   └─ MagicLink.consume(code)でコード検証
   └─ secure_compareでメールアドレス照合
   └─ start_new_session_for(identity)でセッション開始
5. リダイレクト
   └─ サインアップ用途: サインアップ完了画面
   └─ サインイン用途: ダッシュボードまたは元のURL
```

### フローチャート

```mermaid
flowchart TD
    A[ログイン画面] --> B[メールアドレス入力]
    B --> C{Identityの存在確認}
    C -->|存在する| D[マジックリンク送信]
    C -->|存在しない| E{テナント許可?}
    E -->|Yes| F[サインアップ処理]
    E -->|No| G[フェイクリンク画面]
    D --> H[コード入力画面]
    G --> H
    F --> H
    H --> I[コード入力]
    I --> J{コード検証}
    J -->|NG| K[エラー表示]
    K --> I
    J -->|OK| L{メールアドレス照合}
    L -->|NG| M[認証エラー]
    M --> A
    L -->|OK| N[セッション開始]
    N --> O{サインアップ用途?}
    O -->|Yes| P[サインアップ完了画面]
    O -->|No| Q[ダッシュボード]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | マジックリンク有効期限 | 生成から15分間有効 | コード検証時 |
| BR-002 | マジックリンク一回限り | 検証成功時に即座に削除（再利用不可） | コード検証成功時 |
| BR-003 | レート制限（送信） | 3分間に10回まで | メールアドレス送信時 |
| BR-004 | レート制限（検証） | 15分間に10回まで | コード検証時 |
| BR-005 | タイミング攻撃防止 | 存在しないメールでもフェイク画面表示 | テナント制限時 |
| BR-006 | メールアドレス照合 | secure_compareで安全に照合 | コード検証成功時 |

### 計算ロジック

- マジックリンクコード生成: 6桁のランダムな英数字（Code.generate(6)）
- 有効期限: 現在時刻 + 15分（EXPIRATION_TIME.from_now）
- セッショントークン: Session.signed_idで署名付きID生成

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| Identity検索 | identities | SELECT | メールアドレスで既存ユーザー検索 |
| MagicLink作成 | magic_links | INSERT | 6桁コードと有効期限を持つレコード作成 |
| MagicLink消費 | magic_links | SELECT/DELETE | コード検証と削除 |
| Session作成 | sessions | INSERT | 認証成功時にセッションレコード作成 |

### テーブル別操作詳細

#### identities

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | email_address | 入力されたメールアドレス | find_byで検索 |

#### magic_links

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | identity_id | 関連するIdentityのID | |
| INSERT | code | 生成された6桁コード | ユニーク制約あり |
| INSERT | purpose | "sign_in" | サインイン用途（デフォルト） |
| INSERT | expires_at | 現在時刻 + 15分 | |
| SELECT | code, expires_at | code一致かつexpires_at > 現在時刻 | activeスコープ |
| DELETE | - | 検証成功したレコード | consume時に削除 |

#### sessions

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | identity_id | 関連するIdentityのID | |
| INSERT | user_agent | リクエストのUser-Agent | |
| INSERT | ip_address | リクエストのIPアドレス | |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 429 | レート制限超過 | 3分間に10回超過（送信） | "Try again later."メッセージ表示 |
| 429 | レート制限超過 | 15分間に10回超過（検証） | "Try again in 15 minutes."メッセージ表示 |
| 401 | 認証エラー | 無効なマジックリンクコード | shakeアニメーション表示 |
| 401 | 認証エラー | メールアドレス不一致 | "Something went wrong. Please try again."表示 |
| 401 | 認証エラー | セッションにメールアドレスなし | "Enter your email address to sign in."表示 |

### リトライ仕様

- マジックリンクコード入力: 15分間に10回まで（レート制限）
- 無効なコードの場合は再入力可能（同じコード入力画面に戻る）
- メールアドレス不一致の場合はログイン画面に戻る

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

- MagicLink作成・Session作成: 各々単一レコード操作のため自動トランザクション
- MagicLink消費（consume）: destroyとselfの返却を単一メソッドで実行

## パフォーマンス要件

- マジックリンクメール送信: 非同期処理（deliver_later）によりレスポンス即座返却
- レート制限: 3分間10リクエスト（送信）、15分間10リクエスト（検証）でDoS攻撃防止

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

- マジックリンクコード: 6桁のランダム英数字で予測困難
- 有効期限: 15分間の短期間で漏洩リスク軽減
- コード消費: 検証成功時に即座に削除（再利用不可）
- secure_compare使用: メールアドレス照合にタイミング攻撃対策
- フェイクリンク画面: 存在しないメールでも同じ画面表示でアカウント列挙攻撃防止
- セッションCookie: httponly属性、same_site: :lax設定
- レート制限: ブルートフォース攻撃防止

## 備考

- マジックリンクはサインインとサインアップで共通のMagicLinkモデルを使用し、purpose属性で区別
- 新規ユーザーの場合、テナント制限（Account.accepting_signups?）により処理が分岐
- セッション転送機能により、異なるデバイス間でセッションを転送可能

---

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

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

### 推奨読解順序

#### Step 1: データ構造を理解する

まず、サインイン処理で扱うデータモデルの構造を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | identity.rb | `app/models/identity.rb` | ユーザー認証の基盤、send_magic_linkメソッドの実装 |
| 1-2 | magic_link.rb | `app/models/magic_link.rb` | マジックリンクの生成・検証・消費ロジック |
| 1-3 | session.rb | `app/models/session.rb` | セッションモデルの構造 |

**読解のコツ**: MagicLinkモデルのenumでpurpose（sign_in/sign_up）を管理している。activeスコープは有効期限チェックに使用される。

#### Step 2: エントリーポイントを理解する

処理の起点となるコントローラーを特定し、リクエストの流れを把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | sessions_controller.rb | `app/controllers/sessions_controller.rb` | サインイン開始のエントリーポイント、新規/既存ユーザーの分岐 |
| 2-2 | magic_links_controller.rb | `app/controllers/sessions/magic_links_controller.rb` | マジックリンク検証、セッション開始処理 |

**主要処理フロー**:
1. **4行目（sessions_controller.rb）**: レート制限（3分間10回）の設定
2. **11-18行目（sessions_controller.rb）**: createアクションでIdentity検索と処理分岐
3. **53-55行目（sessions_controller.rb）**: sign_inメソッドでマジックリンク送信
4. **12-17行目（magic_links_controller.rb）**: MagicLink.consumeでコード検証
5. **43-50行目（magic_links_controller.rb）**: sign_inメソッドでセッション開始

#### Step 3: 認証基盤を理解する

セッション管理と認証の仕組みを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | authentication.rb | `app/controllers/concerns/authentication.rb` | 認証モジュール、require_unauthenticated_access、セッション管理 |

**主要処理フロー**:
- **16-19行目**: require_unauthenticated_accessでサインイン済みユーザーのリダイレクト
- **88-91行目**: start_new_session_forでセッション作成とCookie設定
- **99-102行目**: terminate_sessionでセッション終了

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

```
SessionsController#create
    │
    ├─ Identity.find_by(email_address:)
    │
    ├─ [既存ユーザー] sign_in(identity)
    │      └─ identity.send_magic_link
    │             ├─ MagicLink.create!(purpose: :sign_in)
    │             │      ├─ generate_code
    │             │      └─ set_expiration
    │             └─ MagicLinkMailer.sign_in_instructions.deliver_later
    │
    ├─ [新規＋テナント許可] sign_up
    │      └─ Signup#create_identity → サインアップ処理
    │
    └─ [新規＋テナント制限] redirect_to_fake_session_magic_link

Sessions::MagicLinksController#create
    │
    ├─ MagicLink.consume(code)
    │      ├─ active.find_by(code:)
    │      └─ destroy (消費)
    │
    ├─ authenticate(magic_link)
    │      └─ secure_compare(email_address, magic_link.identity.email_address)
    │
    └─ sign_in(magic_link)
           ├─ clear_pending_authentication_token
           └─ start_new_session_for(identity)
                  ├─ Session.create!
                  └─ cookies.signed.permanent[:session_token] = session.signed_id
```

### データフロー図

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

email_address ─────▶ SessionsController ─────────────▶ Identity検索
                           │
                           ├──▶ [既存] send_magic_link ──▶ MagicLink（DB）
                           │                                   │
                           │                                   └──▶ Email（外部）
                           │
                           └──▶ [新規] sign_up/fake_link

code ─────────────▶ MagicLinksController ───────────▶ MagicLink検証
                           │                               │
                           ├──▶ authenticate ──────────────▶ メールアドレス照合
                           │
                           └──▶ start_new_session_for ─────▶ Session（DB）
                                                               │
                                                               └──▶ Cookie
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| sessions_controller.rb | `app/controllers/sessions_controller.rb` | コントローラー | サインイン開始・終了処理 |
| magic_links_controller.rb | `app/controllers/sessions/magic_links_controller.rb` | コントローラー | マジックリンク検証 |
| identity.rb | `app/models/identity.rb` | モデル | ユーザー認証基盤 |
| magic_link.rb | `app/models/magic_link.rb` | モデル | マジックリンク管理 |
| session.rb | `app/models/session.rb` | モデル | セッション管理 |
| authentication.rb | `app/controllers/concerns/authentication.rb` | コンサーン | 認証処理 |
| magic_link_mailer.rb | `app/mailers/magic_link_mailer.rb` | メーラー | マジックリンクメール送信 |
| new.html.erb | `app/views/sessions/new.html.erb` | ビュー | ログイン画面 |
| show.html.erb | `app/views/sessions/magic_links/show.html.erb` | ビュー | マジックリンク入力画面 |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義 |
