· Nacho Coll · Guides  · 12 min de lectura

Tokens de Subida a IPFS: Subidas Seguras desde el Cliente sin Exponer Claves API

Aprende cómo los tokens de subida firmados te permiten subir archivos a IPFS de forma segura desde navegadores y aplicaciones móviles sin exponer tu clave API.

Aprende cómo los tokens de subida firmados te permiten subir archivos a IPFS de forma segura desde navegadores y aplicaciones móviles sin exponer tu clave API.

Construir aplicaciones web modernas a menudo requiere subir archivos directamente desde el navegador del usuario al almacenamiento en la nube. Sin embargo, cuando se trata de la seguridad en las subidas a IPFS, los desarrolladores se enfrentan a un dilema desafiante: ¿cómo permites subidas desde el lado del cliente sin exponer tus valiosas claves API a un posible uso indebido?

La mayoría de los servicios de pinning IPFS te obligan a elegir entre opciones incómodas: manejar todas las subidas del lado del servidor (creando cuellos de botella y complejidad) o incrustar tu clave API en el código del cliente (una pesadilla de seguridad). IPFS.NINJA resuelve esto con una característica única que ningún otro servicio de pinning ofrece: tokens de subida firmados.

IPFS Ninja

El Problema con las Claves API Tradicionales de IPFS

Cuando se construyen aplicaciones del lado del cliente que necesitan subir archivos a IPFS, los desarrolladores típicamente se enfrentan a varios desafíos de seguridad:

Riesgo de Exposición de Claves API

Incrustar claves API directamente en el JavaScript del navegador significa que cualquiera puede ver tu código fuente y extraer tus credenciales. Esto podría provocar:

  • Subidas no autorizadas que consumen tu cuota de almacenamiento
  • Abuso potencial de tu cuenta del servicio de pinning
  • Violaciones de cumplimiento de seguridad en entornos empresariales

Cuellos de Botella del Lado del Servidor

La alternativa —enrutar todas las subidas a través de tu backend— genera varios problemas:

  • Mayores costos de ancho de banda del servidor
  • Mayor latencia para los usuarios
  • Requisitos de infraestructura más complejos
  • Posibles puntos únicos de fallo

Seguridad en Aplicaciones Móviles

Las aplicaciones móviles enfrentan desafíos similares, donde las claves API almacenadas en los paquetes de la aplicación pueden ser extraídas mediante ingeniería inversa.

Presentamos los Tokens de Subida a IPFS

Los tokens de subida firmados de IPFS.NINJA proporcionan un punto intermedio seguro. Así es como funcionan:

  1. El servidor genera el token: Tu backend crea un token firmado con tiempo limitado usando tu clave API
  2. El cliente recibe el token: El token se transmite de forma segura a tu aplicación frontend
  3. Subida directa: Los clientes suben directamente a IPFS.NINJA usando el token firmado
  4. Expiración automática: Los tokens expiran después de una duración establecida, limitando la ventana de exposición

Este enfoque combina la seguridad de la autenticación del lado del servidor con los beneficios de rendimiento de las subidas directas del cliente.

Comprendiendo la Seguridad de los Tokens de Subida

Los tokens de subida firmados utilizan firmas criptográficas para garantizar la autenticidad sin exponer tu clave API principal. Cada token contiene:

  • Marca de tiempo de expiración: Invalidación automática después de la duración especificada
  • Restricciones de uso: Límites opcionales en la cantidad de archivos o tamaño total
  • Firma criptográfica: Previene la manipulación o falsificación
  • Verificación del emisor: Se vincula a tu cuenta autenticada

A diferencia de las claves API, los tokens de subida están diseñados para ser incrustados de forma segura en el código del lado del cliente. Incluso si son extraídos, proporcionan acceso limitado que expira automáticamente.

Implementación del Backend: Ejemplo con Express.js

Construyamos un ejemplo completo que muestre cómo implementar subidas seguras a IPFS desde el lado del cliente. Primero, aquí está el backend de Express.js que genera tokens de subida:

// server.js
const express = require('express');
const cors = require('cors');
const app = express();

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

// Your IPFS.NINJA API key (keep this secure on server-side only)
const IPFS_API_KEY = 'bws_1234567890abcdef1234567890abcdef12345678';

// Generate a signed upload token
app.post('/api/generate-upload-token', async (req, res) => {
  try {
    const response = await fetch('https://api.ipfs.ninja/upload-tokens', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Api-Key': IPFS_API_KEY
      },
      body: JSON.stringify({
        expiresIn: '1h', // Token valid for 1 hour
        maxUploads: 10,  // Optional: limit number of uploads
        maxSizeMB: 50    // Optional: limit total upload size
      })
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const tokenData = await response.json();
    
    res.json({
      success: true,
      uploadToken: tokenData.token,
      expiresAt: tokenData.expiresAt
    });
  } catch (error) {
    console.error('Token generation failed:', error);
    res.status(500).json({
      success: false,
      error: 'Failed to generate upload token'
    });
  }
});

// Optional: Endpoint to verify uploads completed successfully
app.post('/api/verify-upload', async (req, res) => {
  const { cid } = req.body;
  
  try {
    // Verify the file was pinned successfully
    const response = await fetch(`https://api.ipfs.ninja/pins/${cid}`, {
      headers: {
        'X-Api-Key': IPFS_API_KEY
      }
    });

    const pinData = await response.json();
    
    res.json({
      success: true,
      verified: pinData.pinned,
      metadata: pinData
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: 'Verification failed'
    });
  }
});

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

Implementación del Frontend: Subida Segura desde el Cliente

Ahora, aquí está el código del frontend que sube archivos de forma segura usando el token firmado:

<!DOCTYPE html>
<html>
<head>
    <title>Secure IPFS Upload Demo</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
        .upload-area { border: 2px dashed #ccc; padding: 20px; text-align: center; margin: 20px 0; }
        .upload-area.dragover { border-color: #007cba; background: #f0f8ff; }
        button { background: #007cba; color: white; border: none; padding: 10px 20px; cursor: pointer; }
        .status { margin: 10px 0; padding: 10px; border-radius: 4px; }
        .success { background: #d4edda; color: #155724; }
        .error { background: #f8d7da; color: #721c24; }
        .info { background: #d1ecf1; color: #0c5460; }
    </style>
</head>
<body>
    <h1>Secure IPFS Upload with Signed Tokens</h1>
    
    <div class="upload-area" id="uploadArea">
        <p>Drag & drop files here or click to select</p>
        <input type="file" id="fileInput" multiple style="display: none;">
        <button onclick="document.getElementById('fileInput').click()">Select Files</button>
    </div>

    <div id="status"></div>
    <div id="results"></div>

    <script>
        class SecureIPFSUploader {
            constructor() {
                this.uploadToken = null;
                this.tokenExpiry = null;
                this.setupEventListeners();
            }

            setupEventListeners() {
                const uploadArea = document.getElementById('uploadArea');
                const fileInput = document.getElementById('fileInput');

                // Drag and drop handlers
                uploadArea.addEventListener('dragover', (e) => {
                    e.preventDefault();
                    uploadArea.classList.add('dragover');
                });

                uploadArea.addEventListener('dragleave', () => {
                    uploadArea.classList.remove('dragover');
                });

                uploadArea.addEventListener('drop', (e) => {
                    e.preventDefault();
                    uploadArea.classList.remove('dragover');
                    this.handleFiles(Array.from(e.dataTransfer.files));
                });

                // File input handler
                fileInput.addEventListener('change', (e) => {
                    this.handleFiles(Array.from(e.target.files));
                });
            }

            async getUploadToken() {
                // Check if we have a valid token
                if (this.uploadToken && this.tokenExpiry && new Date() < new Date(this.tokenExpiry)) {
                    return this.uploadToken;
                }

                try {
                    this.showStatus('Generating secure upload token...', 'info');
                    
                    const response = await fetch('/api/generate-upload-token', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        }
                    });

                    if (!response.ok) {
                        throw new Error(`Failed to generate token: ${response.statusText}`);
                    }

                    const data = await response.json();
                    
                    if (!data.success) {
                        throw new Error(data.error || 'Token generation failed');
                    }

                    this.uploadToken = data.uploadToken;
                    this.tokenExpiry = data.expiresAt;
                    
                    return this.uploadToken;
                } catch (error) {
                    this.showStatus(`Token generation failed: ${error.message}`, 'error');
                    throw error;
                }
            }

            async uploadFile(file) {
                try {
                    const token = await this.getUploadToken();
                    
                    // Convert file to base64 for JSON transport
                    const fileBase64 = await this.fileToBase64(file);
                    
                    this.showStatus(`Uploading ${file.name} to IPFS...`, 'info');

                    const response = await fetch('https://api.ipfs.ninja/upload/new', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'Authorization': `Signed ${token}`
                        },
                        body: JSON.stringify({
                            content: fileBase64,
                            description: `File uploaded via secure token: ${file.name}`,
                            metadata: {
                                filename: file.name,
                                fileType: file.type,
                                uploadedAt: new Date().toISOString(),
                                uploadMethod: 'signed-token'
                            }
                        })
                    });

                    if (!response.ok) {
                        const errorText = await response.text();
                        throw new Error(`Upload failed: ${response.status} ${errorText}`);
                    }

                    const result = await response.json();
                    
                    return {
                        success: true,
                        filename: file.name,
                        cid: result.cid,
                        size: result.sizeMB,
                        ipfsUri: result.uris.ipfs,
                        httpUrl: result.uris.url
                    };
                } catch (error) {
                    return {
                        success: false,
                        filename: file.name,
                        error: error.message
                    };
                }
            }

            async handleFiles(files) {
                if (files.length === 0) return;

                this.clearResults();
                
                try {
                    // Upload files concurrently
                    const uploadPromises = files.map(file => this.uploadFile(file));
                    const results = await Promise.all(uploadPromises);
                    
                    this.displayResults(results);
                    
                    const successful = results.filter(r => r.success).length;
                    const total = results.length;
                    
                    if (successful === total) {
                        this.showStatus(`✅ Successfully uploaded ${successful} file(s) to IPFS!`, 'success');
                    } else {
                        this.showStatus(`⚠️ Uploaded ${successful}/${total} files. Check results below.`, 'error');
                    }
                } catch (error) {
                    this.showStatus(`Upload failed: ${error.message}`, 'error');
                }
            }

            fileToBase64(file) {
                return new Promise((resolve, reject) => {
                    const reader = new FileReader();
                    reader.readAsDataURL(file);
                    reader.onload = () => {
                        // Remove the data:mime/type;base64, prefix
                        const base64 = reader.result.split(',')[1];
                        resolve(base64);
                    };
                    reader.onerror = error => reject(error);
                });
            }

            showStatus(message, type) {
                const statusDiv = document.getElementById('status');
                statusDiv.className = `status ${type}`;
                statusDiv.textContent = message;
            }

            displayResults(results) {
                const resultsDiv = document.getElementById('results');
                
                resultsDiv.innerHTML = '<h3>Upload Results:</h3>' + 
                    results.map(result => `
                        <div class="status ${result.success ? 'success' : 'error'}" style="margin: 10px 0;">
                            <strong>${result.filename}</strong><br>
                            ${result.success ? 
                                `✅ CID: ${result.cid}<br>
                                 📊 Size: ${result.size} MB<br>
                                 🔗 URL: <a href="${result.httpUrl}" target="_blank">${result.httpUrl}</a>` :
                                `❌ Error: ${result.error}`
                            }
                        </div>
                    `).join('');
            }

            clearResults() {
                document.getElementById('results').innerHTML = '';
            }
        }

        // Initialize the uploader
        const uploader = new SecureIPFSUploader();
    </script>
</body>
</html>

Consideraciones Avanzadas de Seguridad

Al implementar seguridad en subidas a IPFS con tokens firmados, considera estas medidas de seguridad adicionales:

Limitaciones del Alcance del Token

Configura los tokens con restricciones apropiadas:

// Generate token with specific constraints
const restrictedToken = await fetch('https://api.ipfs.ninja/upload-tokens', {
  method: 'POST',
  headers: {
    'X-Api-Key': IPFS_API_KEY
  },
  body: JSON.stringify({
    expiresIn: '30m',        // Short expiration
    maxUploads: 5,           // Limited upload count
    maxSizeMB: 10,          // Size restriction
    allowedMimeTypes: ['image/jpeg', 'image/png'], // File type restrictions
    ipWhitelist: ['192.168.1.0/24'] // IP-based access control
  })
});

Validación de Contenido

Siempre valida el contenido subido en tu backend:

app.post('/api/validate-upload', async (req, res) => {
  const { cid } = req.body;
  
  try {
    // Fetch and validate the uploaded content
    const response = await fetch(`https://ipfs.ninja/ipfs/${cid}`);
    const contentType = response.headers.get('content-type');
    
    // Implement your validation logic
    if (!isValidContentType(contentType)) {
      // Remove invalid content
      await deleteFromIPFS(cid);
      return res.status(400).json({ error: 'Invalid content type' });
    }
    
    res.json({ success: true, validated: true });
  } catch (error) {
    res.status(500).json({ error: 'Validation failed' });
  }
});

Limitación de Tasa

Implementa limitación de tasa adicional en tu endpoint de generación de tokens:

const rateLimit = require('express-rate-limit');

const tokenLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 10, // Limit each IP to 10 token requests per windowMs
  message: 'Too many token requests, please try again later'
});

app.use('/api/generate-upload-token', tokenLimiter);

Beneficios Sobre los Enfoques Tradicionales

Los tokens de subida firmados proporcionan varias ventajas sobre los métodos alternativos de seguridad en subidas a IPFS:

vs. Proxy del Lado del Servidor

  • Rendimiento: Las subidas directas eliminan el uso de ancho de banda del servidor
  • Escalabilidad: Sin cuellos de botella del servidor durante períodos de alta carga de subidas
  • Costo: Reducción de costos de ancho de banda y procesamiento
  • Experiencia del Usuario: Mejores velocidades de subida y seguimiento de progreso

vs. Claves API en el Cliente

  • Seguridad: Sin riesgo de extracción o uso indebido de claves API
  • Cumplimiento: Cumple con los requisitos de auditoría de seguridad
  • Control de Acceso: Permisos granulares y expiración automática
  • Monitoreo: Mejor seguimiento de las fuentes y patrones de subida

vs. Otros Servicios de Pinning

IPFS.NINJA es actualmente el único servicio de pinning importante que ofrece tokens de subida firmados. Competidores como Pinata requieren proxy del lado del servidor o exposición de claves API en el cliente, lo que convierte esto en un diferenciador único.

Para más detalles sobre cómo se compara IPFS.NINJA con otros servicios, consulta nuestra guía de comparación completa.

Consejos para el Despliegue en Producción

Al desplegar tokens de subida firmados en producción:

Configuración del Entorno

Almacena la configuración sensible de forma segura:

// Use environment variables for production
const config = {
  ipfsApiKey: process.env.IPFS_API_KEY,
  tokenExpiry: process.env.UPLOAD_TOKEN_EXPIRY || '1h',
  maxFileSize: process.env.MAX_FILE_SIZE_MB || 50,
  allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || ['localhost:3000']
};

Monitoreo y Registro

Implementa un registro completo para monitoreo de seguridad:

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'upload-security.log' })
  ]
});

// Log token generation
logger.info('Upload token generated', {
  userId: req.user.id,
  clientIP: req.ip,
  userAgent: req.get('User-Agent'),
  expiresAt: tokenData.expiresAt
});

Manejo de Errores

Implementa un manejo de errores robusto que no filtre información sensible:

app.use((error, req, res, next) => {
  // Log full error details server-side
  logger.error('Upload token error', {
    error: error.message,
    stack: error.stack,
    userId: req.user?.id,
    endpoint: req.path
  });
  
  // Send safe error message to client
  res.status(500).json({
    success: false,
    error: 'An internal error occurred. Please try again.'
  });
});

Integración con Frameworks Populares

Los tokens de subida firmados funcionan perfectamente con los frameworks web modernos. Aquí tienes ejemplos rápidos de integración:

React Hook

import { useState, useCallback } from 'react';

export function useSecureIPFSUpload() {
  const [uploading, setUploading] = useState(false);
  const [uploadToken, setUploadToken] = useState(null);

  const getToken = useCallback(async () => {
    if (uploadToken?.expiresAt && new Date() < new Date(uploadToken.expiresAt)) {
      return uploadToken.token;
    }

    const response = await fetch('/api/generate-upload-token', {
      method: 'POST'
    });
    const data = await response.json();
    setUploadToken(data);
    return data.uploadToken;
  }, [uploadToken]);

  const uploadFile = useCallback(async (file) => {
    setUploading(true);
    try {
      const token = await getToken();
      // Upload logic here...
    } finally {
      setUploading(false);
    }
  }, [getToken]);

  return { uploadFile, uploading };
}

Vue.js Composable

import { ref } from 'vue';

export function useSecureUpload() {
  const uploading = ref(false);
  const uploadProgress = ref(0);

  const uploadFile = async (file) => {
    uploading.value = true;
    // Implementation here...
  };

  return {
    uploading: readonly(uploading),
    uploadProgress: readonly(uploadProgress),
    uploadFile
  };
}

Conclusión

Los tokens de subida firmados resuelven un desafío de seguridad crítico en el desarrollo de aplicaciones descentralizadas. Al proporcionar una forma segura de habilitar subidas directas desde el cliente a IPFS sin exponer claves API, abren nuevas posibilidades arquitectónicas para aplicaciones web modernas.

Ya sea que estés construyendo un sistema de gestión de contenido, un marketplace de NFT o cualquier aplicación que requiera subidas seguras de archivos, los tokens de subida de IPFS.NINJA proporcionan la seguridad y flexibilidad que necesitas. La implementación es sencilla, los beneficios de seguridad son significativos y las mejoras de rendimiento son sustanciales.

Para aprender más sobre los fundamentos de IPFS, consulta nuestra guía sobre qué es el pinning en IPFS o explora nuestro tutorial completo de la API. Para desarrolladores que evalúan diferentes opciones, nuestra comparativa de los mejores servicios de pinning IPFS proporciona información completa.

¿Listo para implementar subidas seguras a IPFS desde el lado del cliente en tu aplicación? La combinación de prácticas modernas de seguridad y almacenamiento descentralizado hace que este enfoque sea ideal para aplicaciones en producción que necesitan escalar manteniendo estándares de seguridad.

¿Listo para empezar a hacer pinning? Crea una cuenta gratuita — 500 archivos, 1 GB de almacenamiento, gateway dedicado. No se requiere tarjeta de crédito.

Volver al Blog

Artículos Relacionados

Ver Todos los Artículos »