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

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

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

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

📋 目次

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

概要

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

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

統合の準備

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

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

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

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

API呼び出し例
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"
  }'

応答例:

{
  "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
  }
}

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

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

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

コールバックアドレス(redirect_uri)が以下の要件を満たしていることを確認してください。

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

OAuth2 認証フロー

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

詳細手順

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

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

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

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

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

サーバー側コード
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:認証コールバックの処理(サーバー側)

サーバー側で認証コールバックを処理
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:フロントエンドでユーザー情報を取得

フロントエンドがAPIプロキシ経由でユーザー情報を取得
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();
    }
}
サーバー側APIプロキシ
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: 'サーバーエラー' });
    }
});

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

1. 認証エンドポイント

GET /api/oauth2/authorize

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

パラメータ:

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

セキュリティ要件:

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

Scopeの説明:

  • profile:ユーザー基本情報(ユーザー名、メールアドレス)を取得
  • balance:ユーザーアカウント残高情報を取得

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

POST /api/oauth2/token

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

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

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

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

パラメータタイプ必須説明
grant_typestringはい固定値:authorization_code
codestringはい認証コード
redirect_uristringはい認証時のアドレスと一致する必要があります
client_idstringはいアプリケーションのクライアントID
client_secretstringはいアプリケーションのクライアントシークレット(サーバー側でのみ使用)

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

パラメータタイプ必須説明
grant_typestringはい固定値:refresh_token
refresh_tokenstringはいリフレッシュトークン
client_idstringはいアプリケーションのクライアントID
client_secretstringはいアプリケーションのクライアントシークレット(サーバー側でのみ使用)

応答例:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 7200,
  "refresh_token": "refresh_abc123def456...",
  "scope": "profile balance"
}

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

GET /api/oauth2/userinfo

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

リクエストヘッダー:

header
Authorization: Bearer {access_token}

応答例:

response
{
  "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を提供しています。直接使用できます。

<!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ファイルを作成します。

<!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はバックエンドで処理することをお勧めします。

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パラメータの検証

// ランダムな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. トークンの安全な保存

// トークンの有効期限を設定
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. エラー処理

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パラメータを追加するだけです。これにより、ユーザーがログインを完了すると、追加の確認手順なしに自動的に認証が完了します。

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エラーを返した場合、トークンのリフレッシュを自動的に試行し、再試行します
// 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: ユーザーは、当社のユーザーセンター > 認証管理ページで、サードパーティアプリケーションへの認証を取り消すことができます。