· Nacho Coll · Guides  · 11 min de lecture

Tokens d'Upload IPFS : Uploads Sécurisés Côté Client sans Exposer vos Clés API

Découvrez comment les tokens d'upload signés vous permettent de téléverser des fichiers vers IPFS en toute sécurité depuis les navigateurs et applications mobiles sans exposer votre clé API.

Découvrez comment les tokens d'upload signés vous permettent de téléverser des fichiers vers IPFS en toute sécurité depuis les navigateurs et applications mobiles sans exposer votre clé API.

Construire des applications web modernes nécessite souvent de téléverser des fichiers directement depuis les navigateurs des utilisateurs vers le stockage cloud. Cependant, en matière de sécurité des uploads IPFS, les développeurs font face à un dilemme délicat : comment permettre les uploads côté client sans exposer vos précieuses clés API à un usage abusif potentiel ?

La plupart des services de pinning IPFS vous forcent à un choix inconfortable : gérer tous les uploads côté serveur (créant des goulots d’étranglement et de la complexité) ou intégrer votre clé API dans le code client (un cauchemar de sécurité). IPFS.NINJA résout ce problème avec une fonctionnalité unique qu’aucun autre service de pinning n’offre : les tokens d’upload signés.

IPFS Ninja

Le Problème des Clés API IPFS Traditionnelles

Lors de la création d’applications côté client nécessitant des uploads vers IPFS, les développeurs rencontrent généralement plusieurs défis de sécurité :

Risque d’Exposition des Clés API

Intégrer des clés API directement dans le JavaScript du navigateur signifie que n’importe qui peut visualiser votre code source et extraire vos identifiants. Cela pourrait entraîner :

  • Des uploads non autorisés consommant votre quota de stockage
  • Un abus potentiel de votre compte de service de pinning
  • Des violations de conformité en matière de sécurité dans les environnements professionnels

Goulots d’Étranglement Côté Serveur

L’alternative --- router tous les uploads via votre backend --- crée plusieurs problèmes :

  • Des coûts de bande passante serveur plus élevés
  • Une latence accrue pour les utilisateurs
  • Des exigences d’infrastructure plus complexes
  • Des points de défaillance uniques potentiels

Sécurité des Applications Mobiles

Les applications mobiles font face à des défis similaires, où les clés API stockées dans les packages de l’application peuvent être extraites par ingénierie inverse.

Présentation des Tokens d’Upload IPFS

Les tokens d’upload signés d’IPFS.NINJA offrent un juste milieu sécurisé. Voici comment ils fonctionnent :

  1. Le serveur génère le token : Votre backend crée un token signé à durée limitée en utilisant votre clé API
  2. Le client reçoit le token : Le token est transmis en toute sécurité à votre application frontend
  3. Upload direct : Les clients téléversent directement vers IPFS.NINJA en utilisant le token signé
  4. Expiration automatique : Les tokens expirent après une durée définie, limitant la fenêtre d’exposition

Cette approche combine la sécurité de l’authentification côté serveur avec les avantages de performance des uploads directs côté client.

Comprendre la Sécurité des Tokens d’Upload

Les tokens d’upload signés utilisent des signatures cryptographiques pour garantir l’authenticité sans exposer votre clé API principale. Chaque token contient :

  • Horodatage d’expiration : Invalidation automatique après la durée spécifiée
  • Contraintes d’utilisation : Limites optionnelles sur le nombre de fichiers ou la taille totale
  • Signature cryptographique : Empêche la falsification ou la contrefaçon
  • Vérification de l’émetteur : Relie le token à votre compte authentifié

Contrairement aux clés API, les tokens d’upload sont conçus pour être intégrés en toute sécurité dans le code côté client. Même s’ils sont extraits, ils fournissent un accès limité qui expire automatiquement.

Implémentation Backend : Exemple avec Express.js

Construisons un exemple complet montrant comment implémenter des uploads IPFS sécurisés côté client. D’abord, voici le backend Express.js qui génère les tokens d’upload :

// 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');
});

Implémentation Frontend : Upload Client Sécurisé

Voici maintenant le code frontend qui téléverse des fichiers en toute sécurité à l’aide du token signé :

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

Considérations Avancées de Sécurité

Lors de l’implémentation de la sécurité des uploads IPFS avec des tokens signés, considérez ces mesures de sécurité supplémentaires :

Limitations de Portée du Token

Configurez les tokens avec des restrictions appropriées :

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

Validation du Contenu

Validez toujours le contenu téléversé sur votre 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' });
  }
});

Limitation de Débit

Implémentez une limitation de débit supplémentaire sur votre endpoint de génération 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);

Avantages par Rapport aux Approches Traditionnelles

Les tokens d’upload signés offrent plusieurs avantages par rapport aux méthodes alternatives de sécurité des uploads IPFS :

vs. Proxy Côté Serveur

  • Performance : Les uploads directs éliminent l’utilisation de la bande passante serveur
  • Évolutivité : Pas de goulots d’étranglement serveur pendant les périodes de forte charge
  • Coût : Réduction des coûts de bande passante et de traitement
  • Expérience Utilisateur : Meilleures vitesses d’upload et suivi de progression

vs. Clés API Côté Client

  • Sécurité : Aucun risque d’extraction ou d’utilisation abusive des clés API
  • Conformité : Répond aux exigences d’audit de sécurité
  • Contrôle d’Accès : Permissions granulaires et expiration automatique
  • Surveillance : Meilleur suivi des sources et des schémas d’upload

vs. Autres Services de Pinning

IPFS.NINJA est actuellement le seul service de pinning majeur offrant des tokens d’upload signés. Les concurrents comme Pinata nécessitent soit un proxy côté serveur, soit l’exposition des clés API côté client, ce qui en fait un différenciateur unique.

Pour plus de détails sur la comparaison d’IPFS.NINJA avec d’autres services, consultez notre guide de comparaison complet.

Conseils pour le Déploiement en Production

Lors du déploiement de tokens d’upload signés en production :

Configuration de l’Environnement

Stockez les configurations sensibles de manière sécurisée :

// 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']
};

Surveillance et Journalisation

Implémentez une journalisation complète pour la surveillance de la sécurité :

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

Gestion des Erreurs

Implémentez une gestion des erreurs robuste qui ne divulgue pas d’informations sensibles :

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.'
  });
});

Intégration avec les Frameworks Populaires

Les tokens d’upload signés fonctionnent parfaitement avec les frameworks web modernes. Voici des exemples rapides d’intégration :

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

Conclusion

Les tokens d’upload signés résolvent un défi de sécurité critique dans le développement d’applications décentralisées. En fournissant un moyen sécurisé de permettre les uploads directs côté client vers IPFS sans exposer les clés API, ils ouvrent de nouvelles possibilités architecturales pour les applications web modernes.

Que vous construisiez un système de gestion de contenu, une place de marché NFT ou toute application nécessitant des uploads de fichiers sécurisés, les tokens d’upload d’IPFS.NINJA fournissent la sécurité et la flexibilité dont vous avez besoin. L’implémentation est simple, les avantages en matière de sécurité sont significatifs et les gains de performance sont substantiels.

Pour en savoir plus sur les fondamentaux d’IPFS, consultez notre guide sur qu’est-ce que le pinning IPFS ou explorez notre tutoriel complet de l’API. Pour les développeurs évaluant différentes options, notre comparaison des meilleurs services de pinning IPFS fournit des informations complètes.

Prêt à implémenter des uploads IPFS sécurisés côté client dans votre application ? La combinaison de pratiques de sécurité modernes et de stockage décentralisé rend cette approche idéale pour les applications en production qui doivent évoluer tout en maintenant les standards de sécurité.

Prêt à commencer le pinning ? Créez un compte gratuit --- 500 fichiers, 1 Go de stockage, gateway dédié. Aucune carte de crédit requise.

Retour au Blog

Articles Connexes

Voir Tous les Articles »