> ## 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.

# Guide d'intégration tierce

## Déclaration de sécurité importante

Principe « la sécurité d'abord » : lors du développement et du déploiement d'applications tierces, la sécurité est la considération la plus importante. Vous devez suivre strictement les principes de sécurité ci-dessous pour garantir la sécurité des données utilisateurs et du processus d'autorisation.

<Warning>
  1. N'exposez jamais le `client_secret` dans du code JavaScript frontend
  2. L'échange entre code d'autorisation OAuth et token doit être géré côté serveur
  3. Tout accès aux ressources utilisateurs protégées doit passer par un proxy d'API backend
  4. Toutes les communications OAuth doivent être protégées par HTTPS
</Warning>

Toute violation des principes de sécurité ci-dessus peut entraîner de graves vulnérabilités de sécurité !

## 📋 Table des matières

1. [Vue d'ensemble](#overview)
2. [Démarrage](#getting-started)
3. [Flux d'autorisation OAuth2](#oauth2-authorization-flow)
4. [Référence de l'API](#api-reference)
5. [SDK et exemples de code](#sdk-and-code-examples)
6. [Bonnes pratiques de sécurité](#security-best-practices)
7. [FAQ](#faq)
8. [Support technique](#technical-support)

## Vue d'ensemble

Nous fournissons une API ouverte basée sur le standard OAuth2, permettant aux applications tierces d'accéder en toute sécurité aux informations de base et au solde du compte des utilisateurs. Grâce à notre service OAuth2, votre application peut :

* 🚀 Connexion en un clic : les utilisateurs n'ont pas besoin de s'inscrire à nouveau, la connexion accorde automatiquement l'autorisation pour une expérience véritablement fluide
* 👤 Obtenir des informations utilisateur : accéder au profil de base de l'utilisateur (nom d'utilisateur, e-mail, etc.)
* 💰 Consulter le solde du compte : obtenir en temps réel le solde du compte de l'utilisateur
* 🔄 Redirection vers le rechargement : guider les utilisateurs vers notre page de rechargement pour réapprovisionner le compte
* 🔐 Rafraîchissement automatique des tokens : mécanisme intégré de refresh token pour un renouvellement de token transparent, améliorant l'expérience utilisateur

## Démarrage

### 1. Enregistrer un compte développeur

Vous devez d'abord enregistrer un compte développeur dans notre système.

### 2. Créer une application OAuth

Créez votre application OAuth dans la console développeur :

```shell API call example 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": "My Third-party App",
    "description": "App description",
    "redirect_uri": "https://yourapp.com/oauth/callback"
  }'
```

Exemple de réponse :

```json theme={null}
{
  "success": true,
  "message": "Application created successfully",
  "data": {
    "id": 1,
    "name": "My Third-party App",
    "client_id": "client_abc123def456...",
    "client_secret": "secret_xyz789uvw012...",
    "redirect_uri": "https://yourapp.com/oauth/callback",
    "created_time": 1640995200
  }
}
```

Rappel de sécurité important :

<Warning>
  * Le `client_id` peut être utilisé côté frontend (information publique)
  * Le `client_secret` ne peut être utilisé que côté serveur et ne doit jamais être exposé au navigateur
  * Stockez le `client_secret` dans des variables d'environnement ; ne le codez pas en dur dans votre code
</Warning>

### 3. Configurer l'URI de redirection

Assurez-vous que votre URI de redirection répond aux exigences suivantes :

* Utilise le protocole HTTPS (en production)
* Pointe vers un endpoint de votre serveur (et non vers une page frontend)
* Le domaine est enregistré et accessible
* Le chemin est spécifique à l'endpoint API qui traite le callback

## Flux d'autorisation OAuth2

### Diagramme du flux sécurisé

```mermaid theme={null}
sequenceDiagram
    participant Browser  as User Browser
    participant Backend  as Your Server
    participant OAuthSrv as Our Server

    %% Login entry point
    Browser  ->> Backend : GET /auth<br>(user clicks login)

    %% Generate/save state, then redirect
    Backend  ->> Backend : Generate state<br>store state
    Backend  ->> OAuthSrv: Redirect to authorization<br>(with client_id)

    %% Show authorization page and complete
    OAuthSrv -->> Browser : Show authorization page
    Browser  ->> Browser : User completes authorization

    %% Authorization server calls back via browser
    Browser  ->> Backend : GET /callback
    Backend  ->> Backend : Validate state
    Backend  ->> OAuthSrv: Exchange code for token<br>(with secret)
    OAuthSrv -->> Backend : Validate and return token

    %% Save token and return to frontend
    Backend  ->> Backend : Store token
    Backend  -->> Browser : Redirect to frontend

    %% Fetch user info
    Browser  ->> Browser : Show user info
    Browser  ->> Backend : GET /api/user
    Backend  ->> OAuthSrv: Request user info with token
    OAuthSrv -->> Backend : Return user info
    Backend  -->> Browser : Return user info
```

### Instructions pas à pas

#### Étape 1 : Guider l'utilisateur vers l'autorisation

Ajoutez un bouton de connexion sur votre page frontend. Lorsqu'il est cliqué, redirigez vers votre endpoint d'autorisation côté serveur :

```js // Frontend code - only responsible for redirecting theme={null}
function startLogin() {
    // Redirect to your server-side authorization handler
    window.location.href = '/auth/oauth/start';
}
```

#### Étape 2 : Gestionnaire d'autorisation côté serveur

Implémentez le gestionnaire d'autorisation sur votre serveur :

```js // Server-side code theme={null}
app.get('/auth/oauth/start', (req, res) => {
    // Generate and store state parameter
    const state = generateSecureRandomString();
    req.session.oauth_state = state;
    
    // Build authorization 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'); // One-click login
    
    // Redirect to authorization server
    res.redirect(authUrl.toString());
});
```

#### Étape 3 : Gérer le callback d'autorisation (côté serveur)

```js // Server-side authorization callback handler theme={null}
app.get('/oauth/callback', async (req, res) => {
    const { code, state, error } = req.query;
    
    // Error handling
    if (error) {
        return res.redirect(`/?error=${encodeURIComponent(error)}`);
    }
    
    // Parameter validation
    if (!code || !state) {
        return res.redirect('/?error=missing_parameters');
    }
    
    // Validate state parameter (prevent CSRF attacks)
    if (state !== req.session.oauth_state) {
        return res.redirect('/?error=invalid_state');
    }
    
    try {
        // Exchange authorization code for access token (server-side)
        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 // Server-side only
            })
        });
        
        const tokenData = await tokenResponse.json();
        
        if (!tokenResponse.ok) {
            throw new Error(tokenData.error || 'Token exchange failed');
        }
        
        // Securely store token (server session or database)
        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);
        
        // Clean up temporary state
        delete req.session.oauth_state;
        
        // Redirect to frontend page
        res.redirect('/?login=success');
        
    } catch (error) {
        console.error('OAuth callback error:', error);
        res.redirect(`/?error=server_error`);
    }
});
```

#### Étape 4 : Le frontend récupère les informations utilisateur

```js // Frontend fetches user info via API proxy theme={null}
async function loadUserInfo() {
    try {
        const response = await fetch('/api/user/info');
        
        if (!response.ok) {
            throw new Error('Failed to fetch user info');
        }
        
        const userInfo = await response.json();
        displayUserInfo(userInfo);
        
    } catch (error) {
        console.error('Failed to load user info:', error);
        showLoginButton();
    }
}
```

```js // Server-side API proxy theme={null}
app.get('/api/user/info', async (req, res) => {
    const accessToken = req.session.access_token;
    
    if (!accessToken) {
        return res.status(401).json({ error: 'Not authenticated' });
    }
    
    try {
        // Proxy request to OAuth server
        const response = await fetch('https://your-domain.com/api/oauth2/userinfo', {
            headers: {
                'Authorization': `Bearer ${accessToken}`
            }
        });
        
        if (!response.ok) {
            throw new Error('User info request failed');
        }
        
        const userInfo = await response.json();
        res.json(userInfo);
        
    } catch (error) {
        console.error('User info proxy error:', error);
        res.status(500).json({ error: 'Server error' });
    }
});
```

<Tips>
  Expérience de connexion en un clic : en définissant `auto_authorize=true`, après la connexion de l'utilisateur, le système accordera automatiquement l'autorisation sans étape de confirmation supplémentaire.
</Tips>

## Référence de l'API

### 1. Endpoint d'autorisation

GET `/api/oauth2/authorize`

Guide l'utilisateur à travers l'autorisation OAuth2.

Paramètres :

| Paramètre        | Type   | Requis | Description                                                                |
| ---------------- | ------ | ------ | -------------------------------------------------------------------------- |
| `client_id`      | string | Oui    | ID client de l'application (utilisable côté frontend)                      |
| `redirect_uri`   | string | Oui    | URI de callback après autorisation (doit pointer vers un endpoint serveur) |
| `response_type`  | string | Oui    | Fixe : `code`                                                              |
| `scope`          | string | Non    | Portée, séparée par des espaces                                            |
| `state`          | string | Oui    | Chaîne aléatoire pour prévenir les attaques CSRF (générée côté serveur)    |
| `auto_authorize` | string | Non    | Définir à `true` pour activer l'auto-autorisation                          |

Exigences de sécurité :

* `redirect_uri` doit correspondre exactement à l'adresse enregistrée
* `state` doit être une chaîne aléatoire générée côté serveur
* HTTPS doit être utilisé (en production)

Explication des scopes :

* `profile` : accéder aux informations de base de l'utilisateur (nom d'utilisateur, e-mail)
* `balance` : accéder au solde du compte de l'utilisateur

### 2. Endpoint Token

POST `/api/oauth2/token`

<Warning>
  Avertissement de sécurité : cet endpoint ne doit être appelé que depuis le côté serveur, jamais depuis le frontend !
</Warning>

Utilisé dans deux scénarios :

1. Échanger un code d'autorisation contre un access token
2. Utiliser un refresh token pour obtenir un nouvel access token

Paramètres pour la concession « authorization code » :

| Paramètre       | Type   | Requis | Description                                              |
| --------------- | ------ | ------ | -------------------------------------------------------- |
| `grant_type`    | string | Oui    | Fixe : `authorization_code`                              |
| `code`          | string | Oui    | Code d'autorisation                                      |
| `redirect_uri`  | string | Oui    | Doit correspondre à l'URI utilisée pour l'autorisation   |
| `client_id`     | string | Oui    | ID client de l'application                               |
| `client_secret` | string | Oui    | Secret client de l'application (côté serveur uniquement) |

Paramètres pour la concession « refresh token » :

| Paramètre       | Type   | Requis | Description                                              |
| --------------- | ------ | ------ | -------------------------------------------------------- |
| `grant_type`    | string | Oui    | Fixe : `refresh_token`                                   |
| `refresh_token` | string | Oui    | Refresh token                                            |
| `client_id`     | string | Oui    | ID client de l'application                               |
| `client_secret` | string | Oui    | Secret client de l'application (côté serveur uniquement) |

Exemple de réponse :

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

### 3. Endpoint d'informations utilisateur

GET `/api/oauth2/userinfo`

Obtenir les informations de base de l'utilisateur et le solde du compte.

En-tête de requête :

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

Exemple de réponse :

```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 et exemples de code

### SDK JavaScript

Nous fournissons un SDK JavaScript complet que vous pouvez utiliser directement :

```html theme={null}
<!DOCTYPE html>
<html>
<head>
    <title>Third-party App Example</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>
        
        <!-- Loading status -->
        <div id="loading">
            <p>Checking login status...</p>
        </div>
        
        <!-- Logged-in user info -->
        <div id="user-info" class="user-info hidden">
            <h2>Welcome back!</h2>
            <p><strong>Username:</strong> <span id="username"></span></p>
            <p><strong>Email:</strong> <span id="email"></span></p>
            <p><strong>Account Balance:</strong> <span id="balance"></span></p>
            <div style="margin-top: 20px;">
                <button class="btn btn-success" onclick="refreshBalance()">Refresh Balance</button>
                <button class="btn btn-primary" onclick="goToTopup()">Top Up Account</button>
                <button class="btn btn-secondary" onclick="logout()">Log Out</button>
            </div>
        </div>
        
        <!-- Not logged in status -->
        <div id="login-section" class="login-section hidden">
            <h2>Please log in to view account info</h2>
            <p>Use one-click login for quick access to your account</p>
            <button class="btn btn-primary" onclick="oneClickLogin()">
                🚀 One-click Login
            </button>
        </div>
    </div>

    <script>
        // OAuth configuration
        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 {
                        // 检查token是否过期，如果过期尝试刷新
                        if (this.isTokenExpired()) {
                            await this.refreshTokenIfNeeded();
                        }
                        await this.fetchUserInfo();
                        this.showUserInfo();
                    } catch (error) {
                        console.log('Token可能已过期或无效，需要重新登录');
                        this.clearTokens();
                        this.showLoginSection();
                    }
                } else {
                    this.showLoginSection();
                }

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

            // Check if access token is expired
            isTokenExpired() {
                if (!this.tokenExpiresAt) return false;
                const expiryTime = parseInt(this.tokenExpiresAt);
                const bufferTime = 5 * 60 * 1000; // 5 minutes buffer
                return Date.now() > (expiryTime - bufferTime);
            }

            // Automatically refresh token
            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('Token refresh failed');
                    }
                } catch (error) {
                    console.error('Failed to refresh token:', error);
                    this.clearTokens();
                    return false;
                } finally {
                    this.isRefreshing = false;
                }
            }

            // Update token info
            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`; // Enable auto-authorization for true one-click login

                // Open authorization page in popup
                const popup = window.open(authUrl, 'oauth_login', 'width=500,height=600,scrollbars=yes');

                // Listen for popup close
                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 parameter mismatch');
                    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 request method with auto-refresh
            async apiRequest(url, options = {}) {
                // Check and refresh token if needed
                if (this.isTokenExpired()) {
                    const refreshed = await this.refreshTokenIfNeeded();
                    if (!refreshed) {
                        throw new Error('Unable to refresh token');
                    }
                }

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

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

                // If a 401 error is received, try to refresh token and retry once
                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('Failed to get user info');
                }

                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('Balance updated');
                } catch (error) {
                    alert('Refresh failed, please log in again');
                    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('Failed to get top-up link:', 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>
```

### Page de callback OAuth

Créez un fichier `/oauth/callback.html` :

```html theme={null}
<!DOCTYPE html>
<html>
<head>
    <title>Logging in...</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
        .loading { color: #666; }
        .error { color: #dc3545; }
    </style>
</head>
<body>
        <h2>Logging in, please wait...</h2>
        <div id="status" class="loading">Verifying authorization information...</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">Authorization failed: ${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">No valid authorization information received</div>';
            setTimeout(() => {
                window.location.href = '/';
            }, 3000);
        }
    </script>
</body>
</html>
```

### Exemple backend Node.js

Pour une meilleure sécurité, il est recommandé de gérer le `client_secret` côté backend :

```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'
};

// Backend handles token exchange
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: 'Token exchange failed' });
    }
});

// Refresh token endpoint
app.post('/api/oauth/refresh-token', async (req, res) => {
    const { refreshToken } = req.body;

    if (!refreshToken) {
        return res.status(400).json({ error: 'Missing refresh token' });
    }

    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: 'Token refresh failed' });
    }
});

// Proxy user info request (with auto-refresh)
app.get('/api/oauth/userinfo', async (req, res) => {
    const authHeader = req.headers.authorization;
    
    if (!authHeader) {
        return res.status(401).json({ error: 'Missing authorization header' });
    }

    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: 'Failed to fetch user info' });
    }
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
});
```

## Bonnes pratiques de sécurité

### 1. Protection du Client Secret

* ✅ Recommandé : stocker le `client_secret` sur le serveur backend
* ❌ À éviter : exposer le `client_secret` dans du JavaScript frontend

### 2. Validation du paramètre State

```js theme={null}
// Generate random state
const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('oauth_state', state);

// Validate state
const savedState = localStorage.getItem('oauth_state');
if (receivedState !== savedState) {
    throw new Error('CSRF attack detected');
}
```

### 3. Utilisation de HTTPS

* Doit utiliser `HTTPS` en production
* L'URI de callback doit utiliser `HTTPS`
* Toutes les requêtes API utilisent `HTTPS`

### 4. Stockage sécurisé des tokens

```js theme={null}
// Set token expiry time
const expiryTime = Date.now() + (tokenData.expires_in * 1000);
localStorage.setItem('oauth_token_expiry', expiryTime);

// Check if token is expired
function isTokenExpired() {
    const expiry = localStorage.getItem('oauth_token_expiry');
    return !expiry || Date.now() > parseInt(expiry);
}
```

### 5. Gestion des erreurs

```js theme={null}
try {
    const response = await fetch('/api/oauth2/userinfo', {
        headers: { 'Authorization': `Bearer ${token}` }
    });
    
    if (!response.ok) {
        if (response.status === 401) {
            // Token expired, need to re-login
            clearToken();
            showLoginSection();
        } else {
            throw new Error(`HTTP ${response.status}`);
        }
    }
    
    const userInfo = await response.json();
    return userInfo;
} catch (error) {
    console.error('API request failed:', error);
    // Handle network errors etc.
}
```

## FAQ

### Q1 : Comment obtenir une véritable expérience de connexion en un clic ?

R : Il suffit d'ajouter le paramètre `auto_authorize=true` à l'URL d'autorisation. Après la connexion de l'utilisateur, l'autorisation sera accordée automatiquement, sans étape de confirmation supplémentaire :

```js theme={null}
const authUrl = 'https://your-domain.com/api/oauth2/authorize?' +
  'client_id=YOUR_CLIENT_ID&' +
  'auto_authorize=true&' + // Key parameter
  '...other parameters';
```

### Q2 : Les tokens se rafraîchissent-ils automatiquement ?

R : Oui, notre SDK intègre le rafraîchissement automatique des tokens :

* Access token : valide 2 heures, rafraîchissement automatique 5 minutes avant expiration
* Refresh token : valide 30 jours, utilisé pour obtenir de nouveaux access tokens
* Rafraîchissement transparent : tous les appels API vérifient et rafraîchissent automatiquement les tokens expirés
* Nouvelle tentative en cas d'échec : si l'API renvoie une erreur 401, le SDK tentera automatiquement de rafraîchir le token et de réessayer

```js theme={null}
// The SDK will handle token refresh automatically, no manual handling needed
const userInfo = await oauthManager.fetchUserInfo(); // token auto-refresh
```

Ce n'est que si le refresh token expire également que l'utilisateur devra se reconnecter.

### Q3 : Quelles informations utilisateur puis-je obtenir ?

R : Selon le scope autorisé, vous pouvez obtenir :

* Scope `profile` : nom d'utilisateur, e-mail
* Scope `balance` : informations sur le solde du compte

### Q4 : Comment tester l'intégration OAuth ?

R :

1. Utilisez `HTTP localhost` en développement pour les tests
2. Utilisez nos outils de test fournis pour vérifier le flux d'autorisation
3. Vérifiez les requêtes réseau dans les outils de développement du navigateur

### Q5 : Quels langages de programmation sont pris en charge ?

R : Notre API OAuth2 est standard et prend en charge tous les principaux langages de programmation :

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

### Q6 : Comment un utilisateur peut-il révoquer une autorisation ?

## R : Les utilisateurs peuvent révoquer l'autorisation d'une application tierce dans la page Centre utilisateur > Gestion des autorisations.

Dernière mise à jour : 2026-06-01
