Module 4 - Méthodes d'authentification avancées

Maîtrisez les techniques avancées de sécurisation et d'authentification pour WikiJS

Module 4

Introduction à l'authentification avancée

Au-delà des configurations SSO de base, ce module explore les méthodes avancées d'authentification, de sécurisation et de gestion des identités pour WikiJS. Nous verrons les techniques utilisées en production pour sécuriser les applications critiques.

Sujets couverts dans ce module
  • MFA/2FA : Authentification multi-facteurs
  • JWT : Gestion avancée des tokens
  • API Keys : Authentification programmatique
  • Passwordless : Authentification sans mot de passe
  • Fédération : Identités distribuées
  • Stratégies custom : Développements spécifiques
  • Monitoring : Audit et surveillance
  • Zero Trust : Architecture de sécurité moderne
Méthode Sécurité Complexité Use Case
Mot de passe Faible Simple Tests, prototypes
SSO OAuth Moyen Modérée Applications d'entreprise
MFA Élevée Modérée Données sensibles
Certificats Critique Élevée Applications critiques

Authentification Multi-Facteurs (MFA/2FA)

Multi-Factor Authentication

Le MFA ajoute une couche de sécurité supplémentaire en exigeant plusieurs preuves d'identité : quelque chose que vous savez (mot de passe), quelque chose que vous avez (téléphone), quelque chose que vous êtes (biométrie).

TOTP
SMS
Email
Hardware
Time-based One-Time Password (TOTP)
1. Utilisateur se connecte
2. Vérification SSO
3. Demande code TOTP
4. Validation
5. Accès autorisé
Configuration TOTP avec GoogleAuthenticator
# Plugin MFA pour WikiJS
mfa:
  enabled: true
  
  # Méthodes MFA disponibles
  methods:
    - totp
    - sms
    
  # Configuration TOTP
  totp:
    issuer: "MonWiki"
    window: 2  # Tolérance de fenêtre temporelle
    digits: 6  # Nombre de chiffres
    period: 30 # Durée en secondes
    
  # Applications TOTP supportées
  authenticatorApps:
    - "Google Authenticator"
    - "Microsoft Authenticator"
    - "Authy"
    - "1Password"
    
  # Politique de sécurité
  enforcement:
    adminOnly: false      # MFA pour tous ou admin seulement
    gracePeriod: 7        # Jours avant obligation
    backupCodes: 10       # Codes de récupération
Avantages TOTP
  • Offline : Fonctionne sans connexion Internet
  • Standardisé : RFC 6238 compatible
  • Applications multiples : Google, Microsoft, Authy...
  • Sécurisé : Codes temporaires non réutilisables
MFA par SMS
Limitations SMS

Le MFA par SMS est vulnérable aux attaques SIM swapping et interception. Recommandé uniquement comme méthode de fallback.

Configuration SMS avec Twilio
# Configuration SMS MFA
mfa:
  methods:
    - sms
    
  sms:
    provider: "twilio"
    accountSid: "ACXXXXXXXXXXXXXXXXXXXX"
    authToken: "your_twilio_auth_token"
    fromNumber: "+15551234567"
    
    # Template du message
    messageTemplate: "Votre code de vérification: {{code}}"
    
    # Sécurité
    codeLength: 6
    expiryMinutes: 5
    maxAttempts: 3
    
    # Limitation par pays
    allowedCountries: ["FR", "BE", "CH", "CA"]
MFA par Email
Configuration Email MFA
# Configuration Email MFA
mfa:
  methods:
    - email
    
  email:
    # Configuration SMTP
    host: "smtp.gmail.com"
    port: 587
    secure: false
    user: "noreply@mondomaine.com"
    pass: "mot_de_passe_application"
    
    # Template de l'email
    subject: "Code de vérification - {{siteName}}"
    template: |
      Bonjour {{userName}},
      
      Votre code de vérification: {{code}}
      
      Ce code expire dans {{expiryMinutes}} minutes.
      
    # Sécurité
    codeLength: 8
    expiryMinutes: 10
    alphanumeric: true
Clés de sécurité matérielles
WebAuthn / FIDO2

Les clés matérielles (YubiKey, Google Titan, etc.) utilisent les standards WebAuthn et FIDO2 pour une authentification sans phishing.

Configuration WebAuthn
# Configuration WebAuthn/FIDO2
mfa:
  methods:
    - webauthn
    
  webauthn:
    # Configuration du Relying Party
    rpName: "MonWiki"
    rpId: "wiki.mondomaine.com"
    origin: "https://wiki.mondomaine.com"
    
    # Types d'authentificateurs acceptés
    authenticatorSelection:
      authenticatorAttachment: "cross-platform"  # ou "platform"
      userVerification: "required"
      residentKey: "preferred"
      
    # Algorithmes cryptographiques
    supportedAlgorithms: [-7, -35, -36, -257, -258, -259]
    
    # Timeout
    timeout: 60000  # 60 secondes
    
    # Attestation (validation des clés)
    attestation: "none"  # "direct", "indirect", ou "none"
Clés matérielles supportées
  • YubiKey : 5C, 5 NFC, Security Key
  • Google Titan : USB, Bluetooth
  • Feitian : MultiPass FIDO
  • Biométrie : TouchID, FaceID, Windows Hello

JWT et gestion avancée des tokens

JSON Web Tokens (JWT)

Les JWT permettent de transporter des informations de façon sécurisée entre parties. Ils sont signés et optionnellement chiffrés, parfaits pour l'authentification stateless.

Structure d'un JWT
# Structure: HEADER.PAYLOAD.SIGNATURE

# HEADER (encodé en Base64)
{
  "alg": "HS256",
  "typ": "JWT"
}

# PAYLOAD (encodé en Base64)
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022,
  "exp": 1516242622
}

# SIGNATURE
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)
Configuration JWT avancée dans WikiJS
# Configuration JWT personnalisée
jwt:
  # Algorithmes de signature
  algorithm: "RS256"  # ou HS256, ES256, PS256
  
  # Clés de signature/vérification
  privateKey: |
    -----BEGIN RSA PRIVATE KEY-----
    MIIEpAIBAAKCAQEA4f5wg5l2hKsTeNem/V41fGnJm6gOdrj8ym3rFkEjWT2btDK7
    ...
    -----END RSA PRIVATE KEY-----
    
  publicKey: |
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4f5wg5l2hKsTeNem/V41
    ...
    -----END PUBLIC KEY-----
    
  # Configuration des claims
  issuer: "https://wiki.mondomaine.com"
  audience: ["https://wiki.mondomaine.com", "https://api.mondomaine.com"]
  
  # Durée de vie des tokens
  accessTokenTTL: 900    # 15 minutes
  refreshTokenTTL: 86400 # 24 heures
  
  # Claims personnalisés
  customClaims:
    groups: "user.groups"
    department: "user.department"
    clearance: "user.securityClearance"
  
  # Révocation des tokens
  tokenBlacklist:
    enabled: true
    storage: "redis"
    cleanupInterval: 3600
Access/Refresh
Validation
Révocation
Stratégie Access Token / Refresh Token
1. Login initial
2. Access Token (15min)
3. Refresh Token (24h)

4. Token expiré
5. Refresh automatique
6. Nouveau Access Token
Implémentation côté client
// Gestion automatique des tokens côté client
class TokenManager {
  constructor() {
    this.accessToken = localStorage.getItem('accessToken');
    this.refreshToken = localStorage.getItem('refreshToken');
    this.setupAxiosInterceptors();
  }

  setupAxiosInterceptors() {
    // Ajouter le token à chaque requête
    axios.interceptors.request.use(config => {
      if (this.accessToken) {
        config.headers.Authorization = `Bearer ${this.accessToken}`;
      }
      return config;
    });

    // Gérer l'expiration automatiquement
    axios.interceptors.response.use(
      response => response,
      async error => {
        if (error.response?.status === 401) {
          const refreshed = await this.refreshAccessToken();
          if (refreshed) {
            // Relancer la requête originale
            return axios(error.config);
          } else {
            // Rediriger vers login
            window.location.href = '/login';
          }
        }
        return Promise.reject(error);
      }
    );
  }

  async refreshAccessToken() {
    try {
      const response = await axios.post('/api/auth/refresh', {
        refreshToken: this.refreshToken
      });
      
      this.accessToken = response.data.accessToken;
      localStorage.setItem('accessToken', this.accessToken);
      return true;
    } catch (error) {
      this.logout();
      return false;
    }
  }

  logout() {
    this.accessToken = null;
    this.refreshToken = null;
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
  }
}
Validation avancée des JWT
Middleware de validation JWT
// Middleware de validation JWT personnalisé
const jwt = require('jsonwebtoken');
const redis = require('redis');

class JWTValidator {
  constructor(options) {
    this.publicKey = options.publicKey;
    this.algorithms = options.algorithms || ['RS256'];
    this.issuer = options.issuer;
    this.audience = options.audience;
    this.redis = redis.createClient(options.redis);
  }

  async validateToken(token) {
    try {
      // 1. Vérification de la signature et des claims standards
      const decoded = jwt.verify(token, this.publicKey, {
        algorithms: this.algorithms,
        issuer: this.issuer,
        audience: this.audience,
        clockTolerance: 30 // tolérance de 30 secondes
      });

      // 2. Vérifier si le token n'est pas en blacklist
      const isBlacklisted = await this.isTokenBlacklisted(decoded.jti);
      if (isBlacklisted) {
        throw new Error('Token is blacklisted');
      }

      // 3. Vérifications métier personnalisées
      if (!this.validateCustomClaims(decoded)) {
        throw new Error('Custom claims validation failed');
      }

      // 4. Vérifier la freshness (optionnel)
      if (decoded.iat < Date.now() / 1000 - 86400) {
        throw new Error('Token is too old');
      }

      return decoded;
    } catch (error) {
      throw new Error(`JWT validation failed: ${error.message}`);
    }
  }

  async isTokenBlacklisted(jti) {
    if (!jti) return false;
    const result = await this.redis.get(`blacklist:${jti}`);
    return result !== null;
  }

  validateCustomClaims(decoded) {
    // Validation des claims personnalisés
    if (decoded.clearance && !['PUBLIC', 'INTERNAL', 'CONFIDENTIAL'].includes(decoded.clearance)) {
      return false;
    }

    if (decoded.groups && !Array.isArray(decoded.groups)) {
      return false;
    }

    return true;
  }

  async blacklistToken(jti, exp) {
    // Ajouter à la blacklist jusqu'à expiration
    const ttl = exp - Math.floor(Date.now() / 1000);
    if (ttl > 0) {
      await this.redis.setex(`blacklist:${jti}`, ttl, 'true');
    }
  }
}
Révocation des tokens
Défis de la révocation JWT

Les JWT sont stateless par nature, ce qui rend la révocation complexe. Plusieurs stratégies existent pour gérer ce problème.

Stratégies de révocation
# Stratégie 1: Token Blacklist
revocation:
  strategy: "blacklist"
  storage: "redis"
  cleanup: true
  cleanupInterval: 3600

# Stratégie 2: Short-lived tokens avec refresh
revocation:
  strategy: "short-lived"
  accessTokenTTL: 300  # 5 minutes
  refreshTokenRotation: true

# Stratégie 3: Token versioning
revocation:
  strategy: "versioning"
  userTokenVersion: "database"
  globalTokenVersion: "environment"

# Stratégie 4: Claims-based
revocation:
  strategy: "claims"
  activeSessionId: "database"
  deviceFingerprint: true

Authentification par API

API Keys & Programmatic Access

L'authentification par API permet aux applications et scripts d'accéder à WikiJS de façon programmatique, avec un contrôle granulaire des permissions.

API Keys
OAuth Client
Service Accounts
Gestion des clés API
Configuration API Keys dans WikiJS
# Configuration des API Keys
api:
  enabled: true
  
  # Génération des clés
  keyGeneration:
    algorithm: "crypto.randomBytes"
    length: 32
    encoding: "hex"
    prefix: "wjs_"
    
  # Format : wjs_a1b2c3d4e5f6789...
  
  # Permissions par défaut pour les API keys
  defaultPermissions:
    - "read:pages"
    - "read:assets"
    
  # Restrictions
  rateLimiting:
    enabled: true
    windowMs: 900000    # 15 minutes
    maxRequests: 1000   # par fenêtre
    
  # Headers requis
  headers:
    apiKey: "X-API-Key"
    userAgent: "User-Agent"
    
  # Audit
  auditLogs: true
  logRequests: true
Modèle de données API Key
-- Table pour les API Keys
CREATE TABLE api_keys (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    name VARCHAR(255) NOT NULL,
    key_hash VARCHAR(255) UNIQUE NOT NULL,
    key_preview VARCHAR(20) NOT NULL,  -- Premiers caractères pour UI
    
    -- Permissions
    permissions JSON NOT NULL DEFAULT '[]',
    scopes JSON NOT NULL DEFAULT '[]',
    
    -- Restrictions
    ip_whitelist JSON DEFAULT NULL,
    referer_whitelist JSON DEFAULT NULL,
    
    -- Métadonnées
    last_used_at TIMESTAMP NULL,
    expires_at TIMESTAMP NULL,
    
    -- Audit
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    is_active BOOLEAN DEFAULT true
);

-- Index pour les performances
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash);
CREATE INDEX idx_api_keys_user ON api_keys(user_id);
CREATE INDEX idx_api_keys_active ON api_keys(is_active);
Utilisation des API Keys
# Exemple d'utilisation avec curl
# Récupérer une page
curl -H "X-API-Key: wjs_a1b2c3d4e5f6789..." \
     -H "Content-Type: application/json" \
     https://wiki.mondomaine.com/api/pages/home

# Créer une page
curl -X POST \
     -H "X-API-Key: wjs_a1b2c3d4e5f6789..." \
     -H "Content-Type: application/json" \
     -d '{
       "path": "/api-test",
       "title": "Test API",
       "content": "Page créée via API",
       "tags": ["api", "test"]
     }' \
     https://wiki.mondomaine.com/api/pages

# Uploader un fichier
curl -X POST \
     -H "X-API-Key: wjs_a1b2c3d4e5f6789..." \
     -F "file=@document.pdf" \
     https://wiki.mondomaine.com/api/assets
OAuth Client Credentials Flow
1. Client Request
2. Client ID/Secret
3. Access Token
4. API Calls
Flux OAuth Client Credentials
// Client OAuth pour accès programmatique
class WikiJSApiClient {
  constructor(clientId, clientSecret, baseUrl) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.baseUrl = baseUrl;
    this.accessToken = null;
    this.tokenExpiry = null;
  }

  async authenticate() {
    const tokenUrl = `${this.baseUrl}/oauth/token`;
    
    const response = await fetch(tokenUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': `Basic ${btoa(`${this.clientId}:${this.clientSecret}`)}`
      },
      body: new URLSearchParams({
        'grant_type': 'client_credentials',
        'scope': 'read:pages write:pages read:assets'
      })
    });

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

    const data = await response.json();
    this.accessToken = data.access_token;
    this.tokenExpiry = Date.now() + (data.expires_in * 1000);
  }

  async ensureAuthenticated() {
    if (!this.accessToken || Date.now() >= this.tokenExpiry) {
      await this.authenticate();
    }
  }

  async apiCall(method, endpoint, body = null) {
    await this.ensureAuthenticated();

    const response = await fetch(`${this.baseUrl}/api${endpoint}`, {
      method,
      headers: {
        'Authorization': `Bearer ${this.accessToken}`,
        'Content-Type': 'application/json'
      },
      body: body ? JSON.stringify(body) : null
    });

    if (!response.ok) {
      throw new Error(`API call failed: ${response.status}`);
    }

    return response.json();
  }

  // Méthodes helper
  async getPage(path) {
    return this.apiCall('GET', `/pages${path}`);
  }

  async createPage(pageData) {
    return this.apiCall('POST', '/pages', pageData);
  }

  async updatePage(path, pageData) {
    return this.apiCall('PUT', `/pages${path}`, pageData);
  }
}
Comptes de service
Service Accounts

Les comptes de service sont des utilisateurs spéciaux conçus pour les applications plutôt que les humains. Ils ont des permissions spécifiques et des politiques de sécurité adaptées.

Configuration Service Accounts
# Configuration des comptes de service
serviceAccounts:
  enabled: true
  
  # Template par défaut
  defaultTemplate:
    type: "service"
    mustChangePassword: false
    verifyEmail: false
    
  # Restrictions de sécurité
  security:
    # Pas de connexion interactive
    allowInteractiveLogin: false
    
    # IP whitelist obligatoire
    requireIPWhitelist: true
    
    # Rotation automatique des clés
    autoRotateKeys: true
    rotationInterval: 90  # jours
    
    # Audit renforcé
    auditAllActions: true
    
  # Permissions disponibles
  availablePermissions:
    # Pages
    - "read:pages"
    - "write:pages"
    - "delete:pages"
    
    # Assets
    - "read:assets"
    - "write:assets"
    - "delete:assets"
    
    # Users (lecture seulement pour service accounts)
    - "read:users"
    
    # System
    - "read:system"
    - "read:analytics"
Création d'un service account
# Via CLI WikiJS (à implémenter)
wikijs service-account create \
  --name "backup-service" \
  --description "Service de sauvegarde automatique" \
  --permissions "read:pages,read:assets" \
  --ip-whitelist "192.168.1.100,10.0.0.50" \
  --expires-in "1year"

# Via API REST
curl -X POST https://wiki.mondomaine.com/api/admin/service-accounts \
  -H "Authorization: Bearer admin_token" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "integration-service",
    "description": "Service intégration CRM",
    "permissions": ["read:pages", "write:pages"],
    "ipWhitelist": ["203.0.113.0/24"],
    "expiresAt": "2025-12-31T23:59:59Z"
  }'

Authentification sans mot de passe

Passwordless Authentication

L'authentification sans mot de passe utilise la biométrie, les magic links, ou les clés cryptographiques pour éliminer les mots de passe traditionnels.

Magic Links
WebAuthn
Push Auth
WebAuthn / FIDO2 Passwordless
Avantages WebAuthn
  • Phishing-resistant : Impossible à intercepter
  • Pas de partage de secrets : Cryptographie à clé publique
  • Biométrie locale : Empreintes, FaceID restent sur l'appareil
  • Cross-platform : Smartphones, ordinateurs, clés USB
Enregistrement WebAuthn
// Côté client - Enregistrement d'un authentificateur
async function registerWebAuthn() {
  try {
    // Récupérer les options depuis le serveur
    const optionsResponse = await fetch('/api/webauthn/register/begin', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: userEmail })
    });
    const options = await optionsResponse.json();

    // Créer les credentials
    const credential = await navigator.credentials.create({
      publicKey: {
        // Challenge cryptographique
        challenge: base64ToArrayBuffer(options.challenge),
        
        // Informations utilisateur
        user: {
          id: base64ToArrayBuffer(options.user.id),
          name: options.user.name,
          displayName: options.user.displayName
        },
        
        // Informations du Relying Party
        rp: options.rp,
        
        // Algorithmes cryptographiques supportés
        pubKeyCredParams: options.pubKeyCredParams,
        
        // Timeout
        timeout: 60000,
        
        // Sélection de l'authentificateur
        authenticatorSelection: {
          authenticatorAttachment: "platform", // ou "cross-platform"
          userVerification: "required",
          residentKey: "preferred"
        },
        
        // Type d'attestation
        attestation: "direct"
      }
    });

    // Envoyer au serveur pour vérification
    const verificationResponse = await fetch('/api/webauthn/register/finish', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        credential: {
          id: credential.id,
          rawId: arrayBufferToBase64(credential.rawId),
          response: {
            attestationObject: arrayBufferToBase64(credential.response.attestationObject),
            clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON)
          },
          type: credential.type
        }
      })
    });

    if (verificationResponse.ok) {
      alert('Authentificateur enregistré avec succès !');
    }
  } catch (error) {
    console.error('Erreur d\'enregistrement WebAuthn:', error);
  }
}

// Côté client - Authentification
async function authenticateWebAuthn() {
  try {
    const optionsResponse = await fetch('/api/webauthn/authenticate/begin', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: userEmail })
    });
    const options = await optionsResponse.json();

    const assertion = await navigator.credentials.get({
      publicKey: {
        challenge: base64ToArrayBuffer(options.challenge),
        allowCredentials: options.allowCredentials.map(cred => ({
          id: base64ToArrayBuffer(cred.id),
          type: cred.type,
          transports: cred.transports
        })),
        timeout: 60000,
        userVerification: "required"
      }
    });

    const verificationResponse = await fetch('/api/webauthn/authenticate/finish', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        credential: {
          id: assertion.id,
          rawId: arrayBufferToBase64(assertion.rawId),
          response: {
            authenticatorData: arrayBufferToBase64(assertion.response.authenticatorData),
            clientDataJSON: arrayBufferToBase64(assertion.response.clientDataJSON),
            signature: arrayBufferToBase64(assertion.response.signature),
            userHandle: assertion.response.userHandle ? 
              arrayBufferToBase64(assertion.response.userHandle) : null
          },
          type: assertion.type
        }
      })
    });

    if (verificationResponse.ok) {
      window.location.href = '/dashboard';
    }
  } catch (error) {
    console.error('Erreur d\'authentification WebAuthn:', error);
  }
}
Authentification par notification push
1. Tentative de connexion
2. Push vers mobile
3. Approve/Deny
4. Connexion accordée
Push Authentication

Similaire à Duo Mobile ou Microsoft Authenticator, cette méthode envoie une notification push sur le smartphone de l'utilisateur qui peut approuver ou refuser la tentative de connexion.

Configuration Push Auth
# Configuration Push Authentication
passwordless:
  pushAuth:
    enabled: true
    
    # Service de push notifications
    provider: "firebase"  # ou "apns", "custom"
    
    # Configuration Firebase
    firebase:
      projectId: "mon-wikijs-project"
      privateKey: "path/to/service-account.json"
      
    # Configuration des notifications
    notification:
      title: "Demande de connexion"
      body: "Quelqu'un tente de se connecter à {{siteName}}"
      icon: "/assets/logo.png"
      
    # Données contextuelles
    context:
      includeLocation: true
      includeDeviceInfo: true
      includeBrowser: true
      
    # Sécurité
    timeout: 120  # secondes
    maxPendingRequests: 3
    
    # Actions disponibles
    actions:
      - id: "approve"
        title: "Approuver"
        icon: "check"
      - id: "deny"
        title: "Refuser"
        icon: "times"