Next.js×Firebase Authでswr-openapiのmiddlewareを活用したトークン管理

Next.jsでFirebase Authenticationとswr-openapiを組み合わせて使う際、トークンの管理が課題になることが多いです。今回は、Firebase Authのトークン管理に特化して、swr-openapiでのmiddleware活用方法を解説します。

Firebase Authのトークン管理の基本原則

まず重要なのは、トークンをReactのstateで管理しないことです。

なぜstateで管理すべきでないのか

1
2
3
4
5
6
7
8
// ❌ 良くない例: stateでトークンを管理
const [token, setToken] = useState<string | null>(null);

// ✅ 良い例: 必要な時にgetIdToken()で取得
const user = auth.currentUser;
if (user) {
  const token = await user.getIdToken(); // Firebase SDKが自動でリフレッシュ
}

理由:

  • セキュリティ: トークンがメモリに長時間残らない
  • 自動リフレッシュ: Firebase SDKが期限切れを自動判断してリフレッシュ
  • シンプル: 状態管理が複雑にならない

swr-openapiでのmiddleware活用

swr-openapiでFirebase Authのトークンを管理するには、openapi-fetchのmiddlewareを活用するのが最も効率的で柔軟な方法です。

openapi-fetchのmiddleware実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// lib/api-client.ts
import createClient, { type Middleware } from 'openapi-fetch';
import type { paths } from './generated/api'; // openapi-typescriptで生成
import { auth } from './firebase/auth';

interface AuthError extends Error {
  code?: string;
}

const authMiddleware: Middleware = {
  async onRequest({ request }) {
    const user = auth.currentUser;
    
    if (user) {
      try {
        // Firebase SDKが自動的にトークンをリフレッシュ
        const token = await user.getIdToken();
        request.headers.set('Authorization', `Bearer ${token}`);
      } catch (error) {
        console.error('Token取得エラー:', error);
        const authError = error as AuthError;
        if (authError.code === 'auth/network-request-failed') {
          throw new Error('ネットワークエラーが発生しました');
        }
        throw error;
      }
    }
    
    return request;
  },
  
  async onResponse({ response }) {
    // 401エラーの場合は認証エラーとして処理
    if (response.status === 401) {
      throw new Error('認証が必要です');
    }
    return response;
  },
};

// openapi-fetchクライアントを作成してmiddlewareを設定
const client = createClient<paths>({
  baseUrl: process.env.NEXT_PUBLIC_API_URL || '/api',
});

client.use(authMiddleware);

export { client };

条件付き認証の実装

一部のエンドポイントで認証が不要な場合:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// lib/api-client.ts
const UNPROTECTED_ROUTES = [
  '/api/public',
  '/api/health',
  '/api/auth/login',
];

const conditionalAuthMiddleware: Middleware = {
  async onRequest({ request }) {
    const url = new URL(request.url);
    const pathname = url.pathname;
    
    // 認証不要なルートはスキップ
    if (UNPROTECTED_ROUTES.some(route => pathname.startsWith(route))) {
      return request;
    }
    
    const user = auth.currentUser;
    if (!user) {
      throw new Error('認証が必要です');
    }
    
    try {
      const token = await user.getIdToken();
      request.headers.set('Authorization', `Bearer ${token}`);
    } catch (error) {
      console.error('Token取得エラー:', error);
      throw error;
    }
    
    return request;
  },
};

swr-openapiのフック作成

1
2
3
4
5
6
7
// lib/api-hooks.ts
import { createQueryHook, createMutateHook } from 'swr-openapi';
import { client } from './api-client';

// swr-openapiのフックを作成
export const useQuery = createQueryHook(client, 'api');
export const useMutate = createMutateHook(client, 'api');

実際の使用例

基本的な認証付きAPI呼び出し

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// components/UserProfile.tsx
import { useAuth } from '../contexts/AuthContext';
import { useQuery } from '../lib/api-hooks';

interface UserProfile {
  id: string;
  name: string;
  email: string;
}

export default function UserProfile() {
  const { user, loading: authLoading } = useAuth();
  
  // SWRの条件付きフェッチ:userがいない場合はリクエストを送信しない
  const { data, error, isLoading } = useQuery(
    '/api/user/profile',
    user ? {} : null, // userがいない場合はnullを渡してスキップ
  );
  
  if (authLoading) return <div>認証状態を確認中...</div>;
  if (!user) return <div>ログインが必要です</div>;
  if (isLoading) return <div>プロフィール読み込み中...</div>;
  if (error) return <div>エラー: {error.message}</div>;
  
  return (
    <div>
      <h1>こんにちは、{data?.name}さん</h1>
      <p>メール: {data?.email}</p>
    </div>
  );
}

条件付きフェッチのベストプラクティス

なぜmiddlewareではなくuseQuery側で制御するのか:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ middlewareでの制御(技術的には可能だが非推奨)
const authMiddleware: Middleware = {
  async onRequest({ request }) {
    const user = auth.currentUser;
    
    if (!user) {
      // リクエストを送信せず、空のレスポンスを返す
      return new Response(null, { 
        status: 204, // No Content
        headers: { 'X-Skipped': 'true' }
      });
    }
    
    const token = await user.getIdToken();
    request.headers.set('Authorization', `Bearer ${token}`);
    return request;
  },
};

// ✅ swr-openapi側での制御(推奨)
const { data, error, isLoading } = useQuery(
  '/api/user/profile',
  user ? {} : null, // SWRの標準的な条件付きフェッチ
);

推奨する理由:

  • 🎯 SWRの設計思想に沿っている - 条件付きフェッチはSWRの標準機能
  • ⚡ パフォーマンス - リクエスト自体が発生しないため、より効率的
  • 📖 可読性 - コンポーネント側で条件が明確
  • 🐛 デバッグのしやすさ - middleware内の複雑な条件分岐を避けられる

ミューテーション(データ更新)の例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// components/UserProfileEdit.tsx
import { useState } from 'react';
import { useMutate } from '../lib/api-hooks';

interface UpdateUserRequest {
  name?: string;
  email?: string;
}

export default function UserProfileEdit() {
  const [formData, setFormData] = useState<UpdateUserRequest>({});
  const { trigger, isMutating, error } = useMutate(
    'PUT',
    '/api/user/profile'
  );
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    try {
      const result = await trigger({
        body: formData,
      });
      
      if (result.data) {
        alert('プロフィールを更新しました');
      }
    } catch (error) {
      console.error('更新エラー:', error);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="名前"
        value={formData.name || ''}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
      />
      <button type="submit" disabled={isMutating}>
        {isMutating ? '更新中...' : '更新'}
      </button>
      {error && <p>エラー: {error.message}</p>}
    </form>
  );
}

まとめ

Firebase Authとswr-openapiを組み合わせる際の重要なポイント:

🔑 トークン管理のベストプラクティス

  1. stateで管理せず、getIdToken()で都度取得
  2. Firebase SDKの自動リフレッシュ機能を信頼
  3. openapi-fetchのmiddlewareでトークンを自動付与

🚀 openapi-fetch + swr-openapiの利点

  1. 型安全性 - OpenAPIスキーマから自動生成された型安全なAPIクライアント
  2. 柔軟な制御 - middlewareでリクエストレベルの細かい制御
  3. 条件付き認証 - 一部ルートは認証不要に設定可能
  4. パフォーマンス最適化 - トークンキャッシュやリクエストの最適化が容易

この組み合わせにより、型安全で効率的、かつ保守しやすい認証システムを構築できます。Firebase側の重厚な認証ロジックとswr-openapiの型安全性を最大限活用しながら、シンプルで実用的な実装が実現できます。

参考

カテゴリ

comments powered by Disqus