12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Google Cloud】ローカル開発環境からIAP保護されたAPIへ接続する方法

Last updated at Posted at 2025-07-08

⚠️ 重要

  • この記事は実際の開発体験に基づいて、Claude Codeによって執筆されています。
  • 設定方法やエラー対応は環境によって異なる場合があります。

🎯 この記事で解決できること

ローカル開発環境からIAP保護されたAPIへの接続
CORSエラーの解決
認証トークン取得の自動化

🤔 Identity-Aware Proxy(IAP)とは?

Q. そもそもIAP認証って何???

A. IAP(Identity-Aware Proxy) = Googleが提供する「VPNを使わずにアプリを安全に守る仕組み

🏠 自宅のセキュリティに例えると...

  • 従来のVPN:家全体を高い塀で囲む(全部アクセス可能になってしまう)
  • IAP:家の玄関に優秀な警備員を配置(必要な人だけを必要な部屋に案内)

🛠️ 実装手順

【 ステップ1:環境準備 】

1. 📦 必要なパッケージをインストール

Terminal
# Google Auth Library をインストール
yarn add google-auth-library

# npmの方はこちら
npm install google-auth-library

💡 google-auth-libraryとは
Googleの複雑な認証処理を簡単に使えるようにしてくれるライブラリです。

2. 🔧 gcloud CLIの設定

まず、Google Cloud CLIがインストールされているか確認しましょう

Terminal
# バージョンが表示されればOK
gcloud --version

もしインストールされていない場合は、公式ドキュメントからインストールしてください。

インストール済みの方は、以下のコマンドを実行

Terminal
# 1. Google Cloudにログイン(ブラウザが開きます)
gcloud auth login

# 2. Application Default Credentials(ADC)を設定
# これが今回の肝となる設定です!
gcloud auth application-default login

# 3. 使用するプロジェクトを設定
gcloud config set project YOUR_PROJECT_ID

🤓 なぜこの手順が必要?
ADC(Application Default Credentials)は、認証ライブラリが自動的に認証情報を見つけるための仕組みです。ローカル開発環境では、gcloud auth application-default loginで設定した認証情報を使用してGoogle認証が行われます。

3. 🔐 環境変数の設定

.envに追記(ない場合はファイルの作成から)

.env
# IAP保護されたAPIのURL(使用するAPIのURLに書き換えてください)
NEXT_PUBLIC_API_URL="https://your-iap-protected-api.com"

# IAP Client ID(Google Cloud Consoleから取得)
IAP_CLIENT_ID="123456789-abcdefg.apps.googleusercontent.com"

⚠️ 重要

  • NEXT_PUBLIC_がついている変数:ブラウザからアクセス可能(秘密情報は入れちゃダメ)
  • NEXT_PUBLIC_がついていない変数:サーバーサイドのみ(秘密情報OK)

🔍 IAP Client IDの取得方法

  1. Google Cloud Consoleにアクセス
  2. 「セキュリティ」→「Identity-Aware Proxy」
  3. 対象のリソースを選択
  4. 「OAuth クライアントID」の値をコピー

【 ステップ2:IAP認証API実装 】

1. 📁 ディレクトリ構造の確認

まず、以下のディレクトリ構造になっているか確認してください

src/
├── app/
│   └── api/              👈 ここにAPIルートを作成
│       └── iap-token/
│           └── route.ts  👈 新規作成
└── lib/                  👈 ここにユーティリティを作成
    └── iapAuth.ts        👈 新規作成

2. 🏗️ APIルートの実装

src/app/api/iap-token/route.ts を作成

route.ts
import { NextRequest, NextResponse } from 'next/server';
import { GoogleAuth } from 'google-auth-library';

/**
 * 🎯 このAPIルートの役割
 * ローカル開発環境で、IAP認証に必要なIDトークンを取得する
 * 
 * ⚠️ 重要
 * このコードはサーバーサイドで実行されます(ブラウザからは実行されません)
 */
export async function GET(request: NextRequest) {
  try {
    // 環境変数からIAP Client IDを取得
    const targetAudience = process.env.IAP_CLIENT_ID;
    
    // 🚨 環境変数のチェック(よくある設定ミス)
    if (!targetAudience) {
      console.error('❌ IAP_CLIENT_ID環境変数が設定されていません');
      return NextResponse.json(
        { error: 'IAP_CLIENT_ID環境変数が設定されていません' },
        { status: 500 }
      );
    }

    // 🏠 開発環境でのみADCを使用(本番環境は別の認証方法)
    if (process.env.NODE_ENV === 'development') {
      try {
        console.log('🔍 IDトークンを取得中...');
        
        // Google Auth Libraryを使ってIDトークンを取得
        const auth = new GoogleAuth();
        const client = await auth.getIdTokenClient(targetAudience);
        const idToken = await client.idTokenProvider.fetchIdToken(targetAudience);

        console.log('✅ IDトークンの取得に成功しました');
        
        // 🔐 デバッグ用: トークンの中身を確認(開発時のみ)
        if (process.env.NODE_ENV === 'development') {
          const tokenParts = idToken.split('.');
          const payload = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString());
          console.log('🎯 Token audience:', payload.aud);
          console.log('🎯 Expected audience:', targetAudience);
        }

        return NextResponse.json({ token: idToken });
      } catch (error) {
        console.error('❌ IAP認証トークン取得エラー:', error);
        return NextResponse.json(
          { 
            error: 'IDトークンの取得に失敗しました',
            details: error instanceof Error ? error.message : String(error),
            // 🆘 トラブルシューティング用の情報
            help: 'gcloud auth application-default login を実行しましたか?'
          },
          { status: 500 }
        );
      }
    } else {
      // 🚀 本番環境では別の認証方法を実装してください
      return NextResponse.json(
        { error: '本番環境でのIAP認証は未実装です' },
        { status: 501 }
      );
    }
  } catch (error) {
    console.error('💥 予期しないエラー:', error);
    return NextResponse.json(
      { error: '認証処理中にエラーが発生しました' },
      { status: 500 }
    );
  }
}

🤓 なぜAPIルートを作るの?
ブラウザ(フロントエンド)からは、Google Auth Libraryを直接使用できません。そのため、Next.jsのAPIルート(サーバーサイド)で認証処理を行い、フロントエンドは結果だけを受け取る仕組みにしています。


【 ステップ3:ユーティリティ関数実装 】

🔧 認証ヘルパー関数の作成

src/lib/iapAuth.ts を作成

iapAuth.ts
/**
 * --------------------------------------------------
 * 🛠️ IAP認証用のユーティリティ関数集
 * 
 * このファイルには、IAP認証に関する便利な関数をまとめています。
 * 他のファイルから簡単に使えるように設計されています。
 * --------------------------------------------------
 */

/**
 * 🎯 IAP認証用のIDトークンを取得
 * 
 * @returns Promise<string> IDトークン
 * @throws Error 認証に失敗した場合
 */
export const getIapAccessToken = async (): Promise<string> => {
  try {
    console.log('🔄 IAP認証トークンを取得中...');
    
    // 先ほど作成したAPIルートにリクエスト
    const response = await fetch('/api/iap-token', {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      },
    });

    // 🚨 レスポンスのチェック
    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      throw new Error(
        errorData.error || `IAP認証に失敗しました (${response.status})`
      );
    }

    const { token } = await response.json();
    
    // 🚨 トークンの存在チェック
    if (!token) {
      throw new Error('IAP認証トークンの取得に失敗しました');
    }

    console.log('✅ IAP認証トークンを取得しました');
    return token;
  } catch (error) {
    console.error('❌ IAP認証エラー:', error);
    throw error;
  }
};

/**
 * 🤔 IAP認証が必要かどうかを判定
 * 
 * @returns boolean true: IAP認証が必要, false: 不要
 */
export const isIapAuthRequired = (): boolean => {
  const apiUrl = process.env.NEXT_PUBLIC_API_URL;
  
  // 🏠 ローカル環境の場合はIAP認証不要
  if (!apiUrl || apiUrl.includes('localhost') || apiUrl.includes('127.0.0.1')) {
    console.log('🏠 ローカル環境のためIAP認証をスキップします');
    return false;
  }
  
  // 🌐 外部環境の場合はIAP認証が必要
  console.log('🌐 外部環境のためIAP認証が必要です');
  return true;
};

/**
 * 🛡️ API呼び出し用の共通ヘッダーを作成
 * 
 * IAP認証が必要な場合は、Authorizationヘッダーを自動で追加します
 * 
 * @returns Promise<HeadersInit> APIリクエスト用のヘッダー
 */
export const createApiHeaders = async (): Promise<HeadersInit> => {
  const headers: HeadersInit = {};

  // IAP認証が必要な場合のみトークンを取得
  if (isIapAuthRequired()) {
    try {
      const token = await getIapAccessToken();
      headers['Authorization'] = `Bearer ${token}`;
      console.log('🔐 AuthorizationヘッダーをAPIリクエストに追加しました');
    } catch (error) {
      console.error('❌ IAP認証トークンの取得に失敗:', error);
      throw error;
    }
  }

  return headers;
};

💡 設計のポイント
このユーティリティ関数を作ることで、他のファイルからは await createApiHeaders() と一行書くだけで、IAP認証が必要かどうかの判定からトークン取得まで、すべて自動で処理されます!


【 ステップ4:API呼び出しの実装 】

🚀 実際のAPI呼び出しコード

// src/lib/apiClient.ts
import { createApiHeaders } from './iapAuth';

/**
 * 🎯 IAP保護されたAPIを呼び出す関数
 * 
 * この関数一つで、IAP認証の有無を自動判定して適切にAPIを呼び出します
 */
export const callProtectedApi = async (file: File): Promise<Blob> => {
  const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL;
  
  // 🚨 必須チェック
  if (!API_BASE_URL) {
    throw new Error('NEXT_PUBLIC_API_URL環境変数が設定されていません');
  }

  try {
    // 🔐 認証ヘッダーを取得
    const headers = await createApiHeaders();

    console.log('🚀 APIリクエストを開始します...');
    
    // 🌐 APIリクエスト実行
    const response = await fetch(`${API_BASE_URL}/your-api-endpoint`, {
      method: 'GET',
      headers,
      credentials: 'include', // ⭐ 超重要: IAP認証のCookieを送信
    });

    // 🚨 レスポンスチェック
    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`APIエラー (${response.status}): ${errorText}`);
    }

    console.log('✅ APIリクエストが成功しました');
    return await response.blob();
  } catch (error) {
    console.error('❌ API呼び出しエラー:', error);
    throw error;
  }
};

⚠️ 重要な設定
credentials: 'include'絶対に忘れてはいけません!これがないと、IAP認証のCookieが送信されず、認証が失敗します。


🔥 個人的にハマったエラー

😱 CORSエラー

Access to fetch at 'https://your-iap-protected-api.com/your-api-endpoint' 
from origin 'http://localhost:3000' has been blocked by CORS policy: 
Response to preflight request doesn't pass access control check: 
The value of the 'Access-Control-Allow-Credentials' header in the response is '' 
which must be 'true' when the request's credentials mode is 'include'.

✅ 解決方法

1. フロントエンド側(必須)
const response = await fetch(apiUrl, {
  method: 'POST',
  headers: authHeaders,
  body: formData,
  credentials: 'include', // ⭐ これがないと失敗します
});
2. インフラ側の3層設定(参考)
# Layer 1: アプリケーションレイヤー(今回はFastAPIでした)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,  # ← 超重要
    allow_methods=["*"],
    allow_headers=["*"],
)
# Layer 2: Load Balancer
defaultRouteAction:
  corsPolicy:
    allowOrigins: ["http://localhost:3000"]
    allowCredentials: true  # ← これも超重要
    allowMethods: ["GET", "POST", "OPTIONS"]
    allowHeaders: ["Content-Type", "Authorization"]
# Layer 3: IAP
accessSettings:
  corsSettings:
    allowHttpOptions: true  # ← OPTIONSリクエストを許可

📚 今回学んだこと

🔐 認証関連

  1. Google Auth Libraryを使う:ローカル環境でGoogle認証に必須
  2. Next.js APIルートを活用:セキュアなサーバーサイド処理

🌐 CORS攻略の3ステップ

  1. credentials: 'include'を設定:IAP認証のCookieを送信
  2. 3層すべてでCORS設定:アプリ、ロードバランサー、IAP

🔗 さらに学びたい方へ


最後まで読んでくださって、ありがとうございました!

この記事が、あなたの開発の役に立ったなら、ぜひいいね👍やコメント📝をお願いします!

Happy Coding! 🚀

12
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?