> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aihubmix.com/llms.txt
> Use this file to discover all available pages before exploring further.

# サードパーティアプリケーション統合ガイド

## 重要なセキュリティに関する声明

セキュリティ第一の原則：サードパーティアプリケーションの開発と展開において、セキュリティは最も重要な考慮事項です。ユーザーデータと認証プロセスのセキュリティを確保するために、以下のセキュリティ原則を厳守する必要があります。

<Warning>
  1. フロントエンドのJavaScriptコードで`client_secret`を公開することは絶対に禁止されています。
  2. OAuth認証コードをトークンに交換するプロセスは、サーバー側で処理する必要があります。
  3. 保護されたユーザーリソースへのアクセスは、バックエンドAPIプロキシを介して行う必要があります。
  4. すべてのOAuth通信はHTTPSプロトコルで保護する必要があります。
</Warning>

上記のセキュリティ原則のいずれかに違反すると、重大なセキュリティ脆弱性につながる可能性があります！

## 📋 目次

1. [概要](#概要)
2. [統合の準備](#統合の準備)
3. [OAuth2 認証フロー](#oauth2認証フロー)
4. [APIインターフェースドキュメント](#apiインターフェースドキュメント)
5. [SDKとコード例](#sdkとコード例)
6. [セキュリティのベストプラクティス](#セキュリティのベストプラクティス)
7. [よくある質問](#よくある質問)
8. [テクニカルサポート](#テクニカルサポート)

## 概要

当社はOAuth2標準に基づいたオープンAPIを提供しており、サードパーティアプリケーションがユーザーの基本情報とアカウント残高に安全にアクセスできるようにします。当社のOAuth2サービスを通じて、お客様のアプリケーションは以下のことが可能です。

* 🚀 ワンクリックログイン：ユーザーは繰り返し登録する必要がなく、ログインが完了すると自動的に認証され、真のシームレスな体験を提供します。
* 👤 ユーザー情報の取得：ユーザーの基本情報（ユーザー名、メールアドレスなど）にアクセスします。
* 💰 アカウント残高の確認：ユーザーのアカウント残高情報をリアルタイムで取得します。
* 🔄 チャージへのリダイレクト：ユーザーを当社のチャージページに誘導し、アカウントのチャージを行います。
* 🔐 自動トークン更新：リフレッシュトークンメカニズムが組み込まれており、期限切れのトークンを意識することなく更新し、ユーザーエクスペリエンスを向上させます。

## 統合の準備

### 1. 開発者アカウントの登録

まず、当社のシステムで開発者アカウントを登録する必要があります。

### 2. OAuthアプリケーションの作成

開発者コンソールでOAuthアプリケーションを作成します。

```shell API呼び出し例 theme={null}
curl -X POST https://api.aihubmix.com/api/oauth_apps \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_DEVELOPER_TOKEN" \
  -d '{
    "name": "私のサードパーティアプリケーション",
    "description": "アプリケーションの説明情報",
    "redirect_uri": "https://yourapp.com/oauth/callback"
  }'
```

応答例：

```json theme={null}
{
  "success": true,
  "message": "アプリケーションの作成に成功しました",
  "data": {
    "id": 1,
    "name": "私のサードパーティアプリケーション",
    "client_id": "client_abc123def456...",
    "client_secret": "secret_xyz789uvw012...",
    "redirect_uri": "https://yourapp.com/oauth/callback",
    "created_time": 1640995200
  }
}
```

セキュリティに関する重要な注意点：

<Warning>
  * `client_id`はフロントエンドで使用できます（公開情報）
  * `client_secret`はサーバー側でのみ使用でき、ブラウザに公開してはなりません
  * `client_secret`は環境変数に保存し、コードにハードコードしないでください
</Warning>

### 3. コールバックアドレスの設定

コールバックアドレス（redirect\_uri）が以下の要件を満たしていることを確認してください。

* HTTPSプロトコルを使用していること（本番環境）
* サーバー側のエンドポイントを指していること（フロントエンドページではないこと）
* ドメインが登録済みで、正常にアクセスできること
* パスがコールバックを処理するAPIエンドポイントに具体的に指定されていること

## OAuth2 認証フロー

### セキュリティフローチャート

```mermaid theme={null}
sequenceDiagram
    participant Browser  as ユーザーブラウザ
    participant Backend  as お客様のサーバー
    participant OAuthSrv as 当社のサーバー

    %% ログインエントリ
    Browser  ->> Backend : GET /auth<br>(ユーザーがログインをクリック)

    %% stateを生成して保存し、その後リダイレクト
    Backend  ->> Backend : stateを生成<br>stateを保存
    Backend  ->> OAuthSrv: 認証へリダイレクト<br>(client_id付き)

    %% 認証ページを表示し、認証を完了
    OAuthSrv -->> Browser : 認証ページを表示
    Browser  ->> Browser : ユーザーが認証を完了

    %% 認証サーバーがブラウザを介してコールバック
    Browser  ->> Backend : GET /callback
    Backend  ->> Backend : stateを検証
    Backend  ->> OAuthSrv: 認証コードでトークンを交換<br>(secret付き)
    OAuthSrv -->> Backend : 検証してトークンを返す

    %% トークンを保存し、フロントエンドに戻る
    Backend  ->> Backend : トークンを保存
    Backend  -->> Browser : フロントエンドへリダイレクト

    %% ユーザー情報を取得
    Browser  ->> Browser : ユーザー情報を表示
    Browser  ->> Backend : GET /api/user
    Backend  ->> OAuthSrv: トークンでユーザー情報をリクエスト
    OAuthSrv -->> Backend : ユーザー情報を返す
    Backend  -->> Browser : ユーザー情報を返す
```

### 詳細手順

#### ステップ1：ユーザー認証を誘導

フロントエンドページにログインボタンを追加し、クリックするとサーバー側の認証エンドポイントにリダイレクトします。

```js フロントエンドコード - リダイレクトのみを担当 theme={null}
function startLogin() {
    // サーバー側の認証処理エンドポイントにリダイレクト
    window.location.href = '/auth/oauth/start';
}
```

#### ステップ2：サーバー側で認証リクエストを処理

サーバー側で認証処理を実装します。

```js サーバー側コード theme={null}
app.get('/auth/oauth/start', (req, res) => {
    // stateパラメータを生成して保存
    const state = generateSecureRandomString();
    req.session.oauth_state = state;
    
    // 認証URLを構築
    const authUrl = new URL('https://your-domain.com/api/oauth2/authorize');
    authUrl.searchParams.append('client_id', process.env.OAUTH_CLIENT_ID);
    authUrl.searchParams.append('redirect_uri', process.env.OAUTH_REDIRECT_URI);
    authUrl.searchParams.append('response_type', 'code');
    authUrl.searchParams.append('scope', 'profile balance');
    authUrl.searchParams.append('state', state);
    authUrl.searchParams.append('auto_authorize', 'true'); // ワンクリックログイン
    
    // 認証サーバーにリダイレクト
    res.redirect(authUrl.toString());
});
```

#### ステップ3：認証コールバックの処理（サーバー側）

```js サーバー側で認証コールバックを処理 theme={null}
app.get('/oauth/callback', async (req, res) => {
    const { code, state, error } = req.query;
    
    // エラー処理
    if (error) {
        return res.redirect(`/?error=${encodeURIComponent(error)}`);
    }
    
    // パラメータ検証
    if (!code || !state) {
        return res.redirect('/?error=missing_parameters');
    }
    
    // stateパラメータの検証（CSRF攻撃対策）
    if (state !== req.session.oauth_state) {
        return res.redirect('/?error=invalid_state');
    }
    
    try {
        // 認証コードでアクセストークンを交換（サーバー側で完了）
        const tokenResponse = await fetch('https://your-domain.com/api/oauth2/token', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: new URLSearchParams({
                grant_type: 'authorization_code',
                code: code,
                redirect_uri: process.env.OAUTH_REDIRECT_URI,
                client_id: process.env.OAUTH_CLIENT_ID,
                client_secret: process.env.OAUTH_CLIENT_SECRET // サーバー側でのみ使用
            })
        });
        
        const tokenData = await tokenResponse.json();
        
        if (!tokenResponse.ok) {
            throw new Error(tokenData.error || 'トークン交換に失敗しました');
        }
        
        // トークンを安全に保存（サーバー側セッションまたはデータベース）
        req.session.access_token = tokenData.access_token;
        req.session.refresh_token = tokenData.refresh_token;
        req.session.token_expires_at = Date.now() + (tokenData.expires_in * 1000);
        
        // 一時的な状態をクリーンアップ
        delete req.session.oauth_state;
        
        // フロントエンドページにリダイレクト
        res.redirect('/?login=success');
        
    } catch (error) {
        console.error('OAuthコールバックエラー:', error);
        res.redirect(`/?error=server_error`);
    }
});
```

#### ステップ4：フロントエンドでユーザー情報を取得

```js フロントエンドがAPIプロキシ経由でユーザー情報を取得 theme={null}
async function loadUserInfo() {
    try {
        const response = await fetch('/api/user/info');
        
        if (!response.ok) {
            throw new Error('ユーザー情報の取得に失敗しました');
        }
        
        const userInfo = await response.json();
        displayUserInfo(userInfo);
        
    } catch (error) {
        console.error('ユーザー情報の読み込みに失敗しました:', error);
        showLoginButton();
    }
}
```

```js サーバー側APIプロキシ theme={null}
app.get('/api/user/info', async (req, res) => {
    const accessToken = req.session.access_token;
    
    if (!accessToken) {
        return res.status(401).json({ error: '認証されていません' });
    }
    
    try {
        // OAuthサーバーへのプロキシリクエスト
        const response = await fetch('https://your-domain.com/api/oauth2/userinfo', {
            headers: {
                'Authorization': `Bearer ${accessToken}`
            }
        });
        
        if (!response.ok) {
            throw new Error('ユーザー情報のリクエストに失敗しました');
        }
        
        const userInfo = await response.json();
        res.json(userInfo);
        
    } catch (error) {
        console.error('ユーザー情報プロキシエラー:', error);
        res.status(500).json({ error: 'サーバーエラー' });
    }
});
```

<Tips>
  ワンクリックログイン体験：`auto_authorize=true`を設定すると、ユーザーはログイン操作を完了するだけで、システムが追加の確認手順なしに自動的に認証を完了します。
</Tips>

## APIインターフェースドキュメント

### 1. 認証エンドポイント

GET `/api/oauth2/authorize`

ユーザーをOAuth2認証に誘導します。

パラメータ：

| パラメータ            | タイプ    | 必須  | 説明                                      |
| ---------------- | ------ | --- | --------------------------------------- |
| `client_id`      | string | はい  | アプリケーションのクライアントID（フロントエンドで使用可能）         |
| `redirect_uri`   | string | はい  | 認証後のコールバックアドレス（サーバー側のエンドポイントを指す必要があります） |
| `response_type`  | string | はい  | 固定値：`code`                              |
| `scope`          | string | いいえ | 権限範囲、複数ある場合はスペースで区切る                    |
| `state`          | string | はい  | CSRF攻撃対策のランダム文字列（サーバー側で生成）              |
| `auto_authorize` | string | いいえ | `true`に設定すると自動認証が有効になる                  |

セキュリティ要件：

* `redirect_uri`は登録時のアドレスと完全に一致する必要があります
* `state`パラメータはサーバー側で生成されたランダムな文字列である必要があります
* HTTPSプロトコルを使用する必要があります（本番環境）

Scopeの説明：

* `profile`：ユーザー基本情報（ユーザー名、メールアドレス）を取得
* `balance`：ユーザーアカウント残高情報を取得

### 2. トークン取得エンドポイント

POST `/api/oauth2/token`

<Warning>
  セキュリティ警告：このエンドポイントはサーバー側からのみ呼び出すことができ、フロントエンドでは絶対に使用してはなりません！
</Warning>

以下の2つのシナリオで使用されます。

1. 認証コードを使用してアクセストークンを取得
2. リフレッシュトークンを使用して新しいアクセストークンを取得

認証コードフローのパラメータ：

| パラメータ           | タイプ    | 必須 | 説明                                |
| --------------- | ------ | -- | --------------------------------- |
| `grant_type`    | string | はい | 固定値：`authorization_code`          |
| `code`          | string | はい | 認証コード                             |
| `redirect_uri`  | string | はい | 認証時のアドレスと一致する必要があります              |
| `client_id`     | string | はい | アプリケーションのクライアントID                 |
| `client_secret` | string | はい | アプリケーションのクライアントシークレット（サーバー側でのみ使用） |

リフレッシュトークンのパラメータ：

| パラメータ           | タイプ    | 必須 | 説明                                |
| --------------- | ------ | -- | --------------------------------- |
| `grant_type`    | string | はい | 固定値：`refresh_token`               |
| `refresh_token` | string | はい | リフレッシュトークン                        |
| `client_id`     | string | はい | アプリケーションのクライアントID                 |
| `client_secret` | string | はい | アプリケーションのクライアントシークレット（サーバー側でのみ使用） |

応答例：

```json theme={null}
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 7200,
  "refresh_token": "refresh_abc123def456...",
  "scope": "profile balance"
}
```

### 3. ユーザー情報エンドポイント

GET `/api/oauth2/userinfo`

ユーザーの基本情報とアカウント残高を取得します。

リクエストヘッダー：

```json header theme={null}
Authorization: Bearer {access_token}
```

応答例：

```json response theme={null}
{
  "id": 12345,
  "username": "user123",
  "email": "user@example.com",
  "quota": 1000000,
  "used_quota": 250000,
  "balance_formatted": "750.00",
  "created_time": 1640995200,
  "status": 1
}
```

## SDKとコード例

### JavaScript SDK

完全なJavaScript SDKを提供しています。直接使用できます。

```html theme={null}
<!DOCTYPE html>
<html>
<head>
    <title>サードパーティアプリケーションの例</title>
    <style>
        .container { max-width: 800px; margin: 0 auto; padding: 20px; }
        .user-info { background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0; }
        .login-section { text-align: center; padding: 40px; }
        .btn { padding: 12px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; }
        .btn-primary { background: #007bff; color: white; }
        .btn-success { background: #28a745; color: white; }
        .btn-secondary { background: #6c757d; color: white; }
        .hidden { display: none; }
    </style>
</head>
<body>
    <div class="container">
        <h1>私のサードパーティアプリケーション</h1>
        
        <!-- ロード状態 -->
        <div id="loading">
            <p>ログイン状態を確認中...</p>
        </div>
        
        <!-- ログイン済みユーザー情報 -->
        <div id="user-info" class="user-info hidden">
            <h2>おかえりなさい！</h2>
            <p><strong>ユーザー名：</strong><span id="username"></span></p>
            <p><strong>メールアドレス：</strong><span id="email"></span></p>
            <p><strong>アカウント残高：</strong><span id="balance"></span></p>
            <div style="margin-top: 20px;">
                <button class="btn btn-success" onclick="refreshBalance()">残高を更新</button>
                <button class="btn btn-primary" onclick="goToTopup()">アカウントチャージ</button>
                <button class="btn btn-secondary" onclick="logout()">ログアウト</button>
            </div>
        </div>
        
        <!-- 未ログイン状態 -->
        <div id="login-section" class="login-section hidden">
            <h2>アカウント情報を表示するにはログインしてください</h2>
            <p>ワンクリックログインでアカウントに素早くアクセス</p>
            <button class="btn btn-primary" onclick="oneClickLogin()">
                🚀 ワンクリックログイン
            </button>
        </div>
    </div>

    <script>
        // OAuth設定
        const OAUTH_CONFIG = {
            authServer: 'https://your-domain.com',
            clientId: 'YOUR_CLIENT_ID',
            clientSecret: 'YOUR_CLIENT_SECRET', // 本番環境ではバックエンドで処理すべき
            redirectUri: window.location.origin + '/oauth/callback.html',
            scope: 'profile balance'
        };

        class OAuthManager {
            constructor() {
                this.accessToken = localStorage.getItem('oauth_access_token');
                this.refreshToken = localStorage.getItem('oauth_refresh_token');
                this.tokenExpiresAt = localStorage.getItem('oauth_token_expires_at');
                this.isRefreshing = false; // 同時リフレッシュ防止
                this.init();
            }

            async init() {
                // URLに認証コードがあるか確認
                const urlParams = new URLSearchParams(window.location.search);
                const code = urlParams.get('code');
                const state = urlParams.get('state');

                if (code) {
                    await this.handleAuthCallback(code, state);
                    // URLをクリーンアップ
                    window.history.replaceState({}, document.title, window.location.pathname);
                } else if (this.accessToken) {
                    try {
                        // トークンが期限切れか確認し、期限切れの場合はリフレッシュを試みる
                        if (this.isTokenExpired()) {
                            await this.refreshTokenIfNeeded();
                        }
                        await this.fetchUserInfo();
                        this.showUserInfo();
                    } catch (error) {
                        console.log('トークンが期限切れか無効です。再ログインが必要です');
                        this.clearTokens();
                        this.showLoginSection();
                    }
                } else {
                    this.showLoginSection();
                }

                document.getElementById('loading').classList.add('hidden');
            }

            // アクセストークンが期限切れか確認
            isTokenExpired() {
                if (!this.tokenExpiresAt) return false;
                const expiryTime = parseInt(this.tokenExpiresAt);
                const bufferTime = 5 * 60 * 1000; // 5分間のバッファ時間
                return Date.now() > (expiryTime - bufferTime);
            }

            // 必要に応じてトークンを自動リフレッシュ
            async refreshTokenIfNeeded() {
                if (!this.refreshToken || this.isRefreshing) {
                    return false;
                }

                this.isRefreshing = true;
                
                try {
                    const response = await fetch(`${OAUTH_CONFIG.authServer}/api/oauth2/token`, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/x-www-form-urlencoded',
                        },
                        body: new URLSearchParams({
                            grant_type: 'refresh_token',
                            refresh_token: this.refreshToken,
                            client_id: OAUTH_CONFIG.clientId,
                            client_secret: OAUTH_CONFIG.clientSecret,
                        })
                    });

                    if (response.ok) {
                        const tokenData = await response.json();
                        this.updateTokens(tokenData);
                        return true;
                    } else {
                        throw new Error('トークンのリフレッシュに失敗しました');
                    }
                } catch (error) {
                    console.error('トークンのリフレッシュに失敗しました:', error);
                    this.clearTokens();
                    return false;
                } finally {
                    this.isRefreshing = false;
                }
            }

            // トークン情報を更新
            updateTokens(tokenData) {
                this.accessToken = tokenData.access_token;
                this.refreshToken = tokenData.refresh_token;
                this.tokenExpiresAt = Date.now() + (tokenData.expires_in * 1000);

                localStorage.setItem('oauth_access_token', this.accessToken);
                localStorage.setItem('oauth_refresh_token', this.refreshToken);
                localStorage.setItem('oauth_token_expires_at', this.tokenExpiresAt.toString());
            }

            oneClickLogin() {
                const state = this.generateState();
                localStorage.setItem('oauth_state', state);

                const authUrl = `${OAUTH_CONFIG.authServer}/api/oauth2/authorize?` +
                    `client_id=${OAUTH_CONFIG.clientId}&` +
                    `redirect_uri=${encodeURIComponent(OAUTH_CONFIG.redirectUri)}&` +
                    `response_type=code&` +
                    `scope=${encodeURIComponent(OAUTH_CONFIG.scope)}&` +
                    `state=${state}&` +
                    `auto_authorize=true`; // 自動認証を有効にして、真のワンクリックログインを実現

                // ポップアップで認証ページを開く
                const popup = window.open(authUrl, 'oauth_login', 'width=500,height=600,scrollbars=yes');

                // ポップアップのクローズを監視
                const checkClosed = setInterval(() => {
                    if (popup.closed) {
                        clearInterval(checkClosed);
                        // 認証が取得されたか確認
                        setTimeout(() => this.init(), 1000);
                    }
                }, 1000);
            }

            async handleAuthCallback(code, state) {
                const savedState = localStorage.getItem('oauth_state');
                if (state !== savedState) {
                    console.error('Stateパラメータが一致しません');
                    return;
                }

                try {
                    const response = await fetch(`${OAUTH_CONFIG.authServer}/api/oauth2/token`, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/x-www-form-urlencoded',
                        },
                        body: new URLSearchParams({
                            grant_type: 'authorization_code',
                            code: code,
                            client_id: OAUTH_CONFIG.clientId,
                            client_secret: OAUTH_CONFIG.clientSecret,
                            redirect_uri: OAUTH_CONFIG.redirectUri
                        })
                    });

                    const tokenData = await response.json();
                    
                    if (tokenData.access_token) {
                        this.updateTokens(tokenData);
                        localStorage.removeItem('oauth_state');
                        
                        await this.fetchUserInfo();
                        this.showUserInfo();
                    }
                } catch (error) {
                    console.error('アクセストークンの取得に失敗しました:', error);
                }
            }

            // 自動リフレッシュ付きAPIリクエストメソッド
            async apiRequest(url, options = {}) {
                // トークンを確認してリフレッシュ
                if (this.isTokenExpired()) {
                    const refreshed = await this.refreshTokenIfNeeded();
                    if (!refreshed) {
                        throw new Error('トークンのリフレッシュができません');
                    }
                }

                const headers = {
                    'Authorization': `Bearer ${this.accessToken}`,
                    ...options.headers
                };

                const response = await fetch(url, {
                    ...options,
                    headers
                });

                // 401エラーを受け取った場合、トークンをリフレッシュしてもう一度試す
                if (response.status === 401 && !options._retry) {
                    const refreshed = await this.refreshTokenIfNeeded();
                    if (refreshed) {
                        return this.apiRequest(url, { ...options, _retry: true });
                    }
                }

                return response;
            }

            async fetchUserInfo() {
                const response = await this.apiRequest(`${OAUTH_CONFIG.authServer}/api/oauth2/userinfo`);

                if (!response.ok) {
                    throw new Error('ユーザー情報の取得に失敗しました');
                }

                this.userInfo = await response.json();
            }

            showUserInfo() {
                document.getElementById('username').textContent = this.userInfo.username;
                document.getElementById('email').textContent = this.userInfo.email;
                document.getElementById('balance').textContent = this.userInfo.balance_formatted;

                document.getElementById('user-info').classList.remove('hidden');
                document.getElementById('login-section').classList.add('hidden');
            }

            showLoginSection() {
                document.getElementById('user-info').classList.add('hidden');
                document.getElementById('login-section').classList.remove('hidden');
            }

            async refreshBalance() {
                try {
                    await this.fetchUserInfo();
                    document.getElementById('balance').textContent = this.userInfo.balance_formatted;
                    alert('残高が更新されました');
                } catch (error) {
                    alert('リフレッシュに失敗しました。再ログインしてください');
                    this.clearTokens();
                    this.showLoginSection();
                }
            }

            async goToTopup() {
                try {
                    const response = await this.apiRequest(`${OAUTH_CONFIG.authServer}/api/oauth2/topup`);

                    const data = await response.json();
                    if (data.topup_url) {
                        window.open(data.topup_url, '_blank');
                    }
                } catch (error) {
                    console.error('チャージリンクの取得に失敗しました:', error);
                }
            }

            logout() {
                this.clearTokens();
                this.showLoginSection();
            }

            clearTokens() {
                this.accessToken = null;
                this.refreshToken = null;
                this.tokenExpiresAt = null;
                this.userInfo = null;
                
                localStorage.removeItem('oauth_access_token');
                localStorage.removeItem('oauth_refresh_token');
                localStorage.removeItem('oauth_token_expires_at');
            }

            generateState() {
                return Math.random().toString(36).substring(2, 15) +
                       Math.random().toString(36).substring(2, 15);
            }
        }

        // グローバル関数
        let oauthManager;

        function oneClickLogin() {
            oauthManager.oneClickLogin();
        }

        function refreshBalance() {
            oauthManager.refreshBalance();
        }

        function goToTopup() {
            oauthManager.goToTopup();
        }

        function logout() {
            oauthManager.logout();
        }

        // ページ読み込み完了後に初期化
        document.addEventListener('DOMContentLoaded', () => {
            oauthManager = new OAuthManager();
        });
    </script>
</body>
</html>
```

### OAuth コールバックページ

`/oauth/callback.html`ファイルを作成します。

```html theme={null}
<!DOCTYPE html>
<html>
<head>
    <title>ログイン処理中...</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        .loading { color: #666; }
        .error { color: #dc3545; }
    </style>
</head>
<body>
    <h2>ログイン処理中、しばらくお待ちください...</h2>
    <div id="status" class="loading">認証情報を検証中...</div>

    <script>
        const urlParams = new URLSearchParams(window.location.search);
        const code = urlParams.get('code');
        const state = urlParams.get('state');
        const error = urlParams.get('error');

        if (error) {
            document.getElementById('status').innerHTML =
                `<div class="error">認証失敗: ${error}</div>`;
            setTimeout(() => {
                if (window.opener) {
                    window.close();
                } else {
                    window.location.href = '/';
                }
            }, 3000);
        } else if (code) {
            if (window.opener) {
                // ポップアップの場合、閉じて親ウィンドウに処理させる
                window.opener.location.href =
                    window.opener.location.pathname + `?code=${code}&state=${state}`;
                window.close();
            } else {
                // ポップアップでない場合、メインページにリダイレクト
                window.location.href = `/?code=${code}&state=${state}`;
            }
        } else {
            document.getElementById('status').innerHTML =
                '<div class="error">有効な認証情報が受信されませんでした</div>';
            setTimeout(() => {
                window.location.href = '/';';
            }, 3000);
        }
    </script>
</body>
</html>
```

### Node.js バックエンドの例

セキュリティを向上させるため、`client_secret`はバックエンドで処理することをお勧めします。

```js theme={null}
const express = require('express');
const cors = require('cors');
const fetch = require('node-fetch');

const app = express();
app.use(cors());
app.use(express.json());

const OAUTH_CONFIG = {
    authServer: 'https://your-domain.com',
    clientId: 'YOUR_CLIENT_ID',
    clientSecret: 'YOUR_CLIENT_SECRET'
};

// バックエンドでトークン交換を処理
app.post('/api/oauth/exchange-token', async (req, res) => {
    const { code, redirectUri, state } = req.body;

    try {
        const response = await fetch(`${OAUTH_CONFIG.authServer}/api/oauth2/token`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: new URLSearchParams({
                grant_type: 'authorization_code',
                code: code,
                client_id: OAUTH_CONFIG.clientId,
                client_secret: OAUTH_CONFIG.clientSecret,
                redirect_uri: redirectUri
            })
        });

        const tokenData = await response.json();
        
        if (response.ok) {
            res.json(tokenData);
        } else {
            res.status(400).json(tokenData);
        }
    } catch (error) {
        res.status(500).json({ error: 'トークン交換に失敗しました' });
    }
});

// トークンリフレッシュエンドポイント
app.post('/api/oauth/refresh-token', async (req, res) => {
    const { refreshToken } = req.body;

    if (!refreshToken) {
        return res.status(400).json({ error: 'リフレッシュトークンがありません' });
    }

    try {
        const response = await fetch(`${OAUTH_CONFIG.authServer}/api/oauth2/token`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: new URLSearchParams({
                grant_type: 'refresh_token',
                refresh_token: refreshToken,
                client_id: OAUTH_CONFIG.clientId,
                client_secret: OAUTH_CONFIG.clientSecret,
            })
        });

        const tokenData = await response.json();
        
        if (response.ok) {
            res.json(tokenData);
        } else {
            res.status(400).json(tokenData);
        }
    } catch (error) {
        res.status(500).json({ error: 'トークンリフレッシュに失敗しました' });
    }
});

// ユーザー情報リクエストのプロキシ（自動リフレッシュ付き）
app.get('/api/oauth/userinfo', async (req, res) => {
    const authHeader = req.headers.authorization;
    
    if (!authHeader) {
        return res.status(401).json({ error: '認証ヘッダーがありません' });
    }

    try {
        const response = await fetch(`${OAUTH_CONFIG.authServer}/api/oauth2/userinfo`, {
            headers: {
                'Authorization': authHeader
            }
        });

        const userData = await response.json();
        
        if (response.ok) {
            res.json(userData);
        } else {
            res.status(response.status).json(userData);
        }
    } catch (error) {
        res.status(500).json({ error: 'ユーザー情報の取得に失敗しました' });
    }
});

app.listen(3000, () => {
    console.log('サーバーがポート3000で実行中');
});
```

## セキュリティのベストプラクティス

### 1. クライアントシークレットの保護

* ✅ 推奨：`client_secret`をバックエンドサーバーに保存
* ❌ 回避：フロントエンドのJavaScriptで`client_secret`を公開

### 2. Stateパラメータの検証

```js theme={null}
// ランダムなstateを生成
const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('oauth_state', state);

// stateを検証
const savedState = localStorage.getItem('oauth_state');
if (receivedState !== savedState) {
    throw new Error('CSRF攻撃が検出されました');
}
```

### 3. HTTPSの使用

* 本番環境では`HTTPS`を使用する必要があります
* コールバックアドレスは`HTTPS`を使用する必要があります
* すべてのAPIリクエストは`HTTPS`を使用します

### 4. トークンの安全な保存

```js theme={null}
// トークンの有効期限を設定
const expiryTime = Date.now() + (tokenData.expires_in * 1000);
localStorage.setItem('oauth_token_expiry', expiryTime);

// トークンが期限切れか確認
function isTokenExpired() {
    const expiry = localStorage.getItem('oauth_token_expiry');
    return !expiry || Date.now() > parseInt(expiry);
}
```

### 5. エラー処理

```js theme={null}
try {
    const response = await fetch('/api/oauth2/userinfo', {
        headers: { 'Authorization': `Bearer ${token}` }
    });
    
    if (!response.ok) {
        if (response.status === 401) {
            // トークン期限切れ、再ログインが必要
            clearToken();
            showLoginSection();
        } else {
            throw new Error(`HTTP ${response.status}`);
        }
    }
    
    const userInfo = await response.json();
    return userInfo;
} catch (error) {
    console.error('APIリクエストに失敗しました:', error);
    // ネットワークエラーなどを処理
}
```

## よくある質問

### Q1: 真のワンクリックログイン体験を実現するにはどうすればよいですか？

A: 認証URLに`auto_authorize=true`パラメータを追加するだけです。これにより、ユーザーがログインを完了すると、追加の確認手順なしに自動的に認証が完了します。

```js theme={null}
const authUrl = 'https://your-domain.com/api/oauth2/authorize?' +
  'client_id=YOUR_CLIENT_ID&' +
  'auto_authorize=true&' + // 重要なパラメータ
  '...その他のパラメータ';
```

### Q2: トークンは自動的にリフレッシュされますか？

A: はい、当社のSDKには自動トークンリフレッシュメカニズムが組み込まれています。

* アクセストークン：2時間有効、期限切れの5分前に自動リフレッシュ
* リフレッシュトークン：30日間有効、新しいアクセストークンを取得するために使用
* シームレスなリフレッシュ：すべてのAPI呼び出しは、期限切れのトークンを自動的にチェックしてリフレッシュします
* 失敗時の再試行：APIが401エラーを返した場合、トークンのリフレッシュを自動的に試行し、再試行します

```js theme={null}
// SDKがトークンのリフレッシュを自動的に処理するため、手動で処理する必要はありません
const userInfo = await oauthManager.fetchUserInfo(); // 自動的にトークンをリフレッシュ
```

リフレッシュトークンも期限切れになった場合にのみ、ユーザーは再ログインする必要があります。

### Q3: どのようなユーザー情報を取得できますか？

A: 認証スコープに応じて、以下を取得できます。

* `profile`スコープ：ユーザー名、メールアドレス
* `balance`スコープ：アカウント残高情報

### Q4: OAuth統合をテストするにはどうすればよいですか？

A:

1. 開発環境で`HTTP localhost`を使用してテストします
2. 提供されているテストツールを使用して認証フローを検証します
3. ブラウザの開発者ツールでネットワークリクエストを確認します

### Q5: どのプログラミング言語をサポートしていますか？

A: 当社は標準のOAuth2 APIを提供しており、すべての主要なプログラミング言語をサポートしています。

* JavaScript/Node.js
* Python
* PHP
* Java
* C#/.NET
* Go
* Ruby

### Q6: ユーザー認証を取り消すにはどうすればよいですか？

## A: ユーザーは、当社のユーザーセンター > 認証管理ページで、サードパーティアプリケーションへの認証を取り消すことができます。

最終更新日：2026-06-01
