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

# Guía de integración con terceros

## Aviso de seguridad importante

Principio de seguridad ante todo: al desarrollar y desplegar aplicaciones de terceros, la seguridad es la consideración más importante. Debes seguir estrictamente los principios de seguridad expuestos a continuación para garantizar la seguridad de los datos de los usuarios y del proceso de autorización.

<Warning>
  1. Nunca expongas `client_secret` en el código JavaScript del frontend
  2. El intercambio del código de autorización OAuth por el token debe gestionarse en el servidor
  3. Todo acceso a recursos protegidos del usuario debe pasar por un proxy de API en el backend
  4. Toda comunicación OAuth debe protegerse mediante HTTPS
</Warning>

¡Violar cualquiera de los principios de seguridad anteriores puede provocar vulnerabilidades de seguridad graves!

## 📋 Índice

1. [Descripción general](#overview)
2. [Primeros pasos](#getting-started)
3. [Flujo de autorización OAuth2](#oauth2-authorization-flow)
4. [Referencia de la API](#api-reference)
5. [SDK y ejemplos de código](#sdk-and-code-examples)
6. [Buenas prácticas de seguridad](#security-best-practices)
7. [Preguntas frecuentes](#faq)
8. [Soporte técnico](#technical-support)

## Descripción general

Ofrecemos una API abierta basada en el estándar OAuth2 que permite a las aplicaciones de terceros acceder de forma segura a la información básica de los usuarios y al saldo de su cuenta. A través de nuestro servicio OAuth2, tu aplicación puede:

* 🚀 Inicio de sesión con un clic: los usuarios no necesitan registrarse de nuevo; el inicio de sesión concede automáticamente la autorización, proporcionando una experiencia verdaderamente fluida
* 👤 Obtener información del usuario: acceder al perfil básico de los usuarios (nombre de usuario, correo electrónico, etc.)
* 💰 Ver el saldo de la cuenta: obtener el saldo de la cuenta del usuario en tiempo real
* 🔄 Redirección a recarga: guía al usuario a nuestra página de recarga para recargar su cuenta
* 🔐 Renovación automática de tokens: mecanismo integrado de refresh token para una renovación sin interrupciones, mejorando la experiencia del usuario

## Primeros pasos

### 1. Registra una cuenta de desarrollador

Primero, debes registrar una cuenta de desarrollador en nuestro sistema.

### 2. Crea una aplicación OAuth

Crea tu aplicación OAuth en la consola de desarrolladores:

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

Ejemplo de respuesta:

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

Aviso de seguridad importante:

<Warning>
  * client\_id puede utilizarse en el frontend (información pública)
  * `client_secret` solo puede utilizarse del lado del servidor y nunca debe exponerse al navegador
  * Almacena client\_secret en variables de entorno; no lo dejes en duro en el código
</Warning>

### 3. Configura el Redirect URI

Asegúrate de que tu URI de redirección cumpla los siguientes requisitos:

* Utilice el protocolo HTTPS (en producción)
* Apunte a un endpoint de tu servidor (no a una página de frontend)
* El dominio esté registrado y sea accesible
* La ruta sea específica del endpoint de API que gestiona la callback

## Flujo de autorización OAuth2

### Diagrama de flujo seguro

```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
```

### Instrucciones paso a paso

#### Paso 1: Guía al usuario para que autorice

Añade un botón de inicio de sesión en tu página de frontend. Al hacer clic, redirige al endpoint de autorización del lado del servidor:

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

#### Paso 2: Manejador de autorización en el servidor

Implementa el manejador de autorización en tu servidor:

```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());
});
```

#### Paso 3: Gestionar la callback de autorización (del lado del servidor)

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

#### Paso 4: El frontend obtiene la información del usuario

```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>
  Experiencia de inicio de sesión con un clic: al establecer `auto_authorize=true`, después de que el usuario inicie sesión, el sistema concederá automáticamente la autorización sin pasos adicionales de confirmación.
</Tips>

## Referencia de la API

### 1. Endpoint de autorización

GET `/api/oauth2/authorize`

Guía al usuario a través de la autorización OAuth2.

Parámetros:

| Parámetro        | Tipo   | Obligatorio | Descripción                                                                  |
| ---------------- | ------ | ----------- | ---------------------------------------------------------------------------- |
| `client_id`      | string | Sí          | ID de cliente de la aplicación (puede usarse en el frontend)                 |
| `redirect_uri`   | string | Sí          | URI de callback tras la autorización (debe apuntar al endpoint del servidor) |
| `response_type`  | string | Sí          | Fijo: `code`                                                                 |
| `scope`          | string | No          | Ámbito, separado por espacios                                                |
| `state`          | string | Sí          | Cadena aleatoria para prevenir ataques CSRF (generada en el servidor)        |
| `auto_authorize` | string | No          | Establecer a `true` para activar la autorización automática                  |

Requisitos de seguridad:

* `redirect_uri` debe coincidir exactamente con la dirección registrada
* `state` debe ser una cadena aleatoria generada en el servidor
* Se debe usar HTTPS (en producción)

Explicación de los scopes:

* `profile`: Acceso a la información básica del usuario (nombre de usuario, correo electrónico)
* `balance`: Acceso al saldo de la cuenta del usuario

### 2. Endpoint del token

POST `/api/oauth2/token`

<Warning>
  Aviso de seguridad: ¡este endpoint solo puede llamarse desde el lado del servidor, nunca desde el frontend!
</Warning>

Se utiliza en dos escenarios:

1. Intercambiar el código de autorización por un access token
2. Usar el refresh token para obtener un nuevo access token

Parámetros para la concesión por código de autorización:

| Parámetro       | Tipo   | Obligatorio | Descripción                                                      |
| --------------- | ------ | ----------- | ---------------------------------------------------------------- |
| `grant_type`    | string | Sí          | Fijo: `authorization_code`                                       |
| `code`          | string | Sí          | Código de autorización                                           |
| `redirect_uri`  | string | Sí          | Debe coincidir con la URI usada en la autorización               |
| `client_id`     | string | Sí          | ID de cliente de la aplicación                                   |
| `client_secret` | string | Sí          | Secreto de cliente de la aplicación (solo del lado del servidor) |

Parámetros para la concesión por refresh token:

| Parámetro       | Tipo   | Obligatorio | Descripción                                                      |
| --------------- | ------ | ----------- | ---------------------------------------------------------------- |
| `grant_type`    | string | Sí          | Fijo: `refresh_token`                                            |
| `refresh_token` | string | Sí          | Refresh token                                                    |
| `client_id`     | string | Sí          | ID de cliente de la aplicación                                   |
| `client_secret` | string | Sí          | Secreto de cliente de la aplicación (solo del lado del servidor) |

Ejemplo de respuesta:

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

### 3. Endpoint de información del usuario

GET `/api/oauth2/userinfo`

Obtén la información básica del usuario y el saldo de su cuenta.

Cabecera de la solicitud:

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

Ejemplo de respuesta:

```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 y ejemplos de código

### SDK de JavaScript

Ofrecemos un SDK completo de JavaScript que puedes utilizar directamente:

```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>
```

### Página de callback de OAuth

Crea un archivo `/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>
```

### Ejemplo de backend en Node.js

Para mayor seguridad, se recomienda gestionar `client_secret` en el 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');
});
```

## Buenas prácticas de seguridad

### 1. Protección del client secret

* ✅ Recomendado: Almacena `client_secret` en el servidor backend
* ❌ Evitar: Exponer `client_secret` en JavaScript del frontend

### 2. Validación del parámetro 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. Uso de HTTPS

* Debe usarse `HTTPS` en producción
* El URI de callback debe usar `HTTPS`
* Todas las solicitudes a la API usan `HTTPS`

### 4. Almacenamiento seguro de 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. Manejo de errores

```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.
}
```

## Preguntas frecuentes

### P1: ¿Cómo conseguir una verdadera experiencia de inicio de sesión con un clic?

R: Solo tienes que añadir el parámetro `auto_authorize=true` a la URL de autorización. Después de que el usuario inicie sesión, la autorización se concederá automáticamente, sin ningún paso adicional de confirmación:

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

### P2: ¿Los tokens se renuevan automáticamente?

R: Sí, nuestro SDK incorpora la renovación automática de tokens:

* Access token: válido durante 2 horas, se renueva automáticamente 5 minutos antes de su expiración
* Refresh token: válido durante 30 días, se utiliza para obtener nuevos access tokens
* Renovación sin interrupciones: Todas las llamadas a la API comprobarán y renovarán automáticamente los tokens caducados
* Reintento en caso de fallo: Si la API devuelve un error 401, intentará renovar automáticamente el token y reintentar

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

Solo si el refresh token también caduca el usuario deberá iniciar sesión de nuevo.

### P3: ¿Qué información del usuario puedo obtener?

R: Según el alcance autorizado, puedes obtener:

* Scope `profile`: nombre de usuario, correo electrónico
* Scope `balance`: información del saldo de la cuenta

### P4: ¿Cómo puedo probar la integración OAuth?

R:

1. Usa `HTTP localhost` en desarrollo para realizar pruebas
2. Utiliza nuestras herramientas de prueba proporcionadas para verificar el flujo de autorización
3. Comprueba las solicitudes de red en las herramientas de desarrollador de tu navegador

### P5: ¿Qué lenguajes de programación se admiten?

R: Nuestra API OAuth2 es estándar y admite todos los lenguajes de programación principales:

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

### P6: ¿Cómo puede un usuario revocar la autorización?

## R: Los usuarios pueden revocar la autorización de aplicaciones de terceros en la página Centro de usuarios > Gestión de autorizaciones.

Última actualización: 2026-06-01
