Module 4 - Méthodes d'authentification avancées
Maîtrisez les techniques avancées de sécurisation et d'authentification pour WikiJS
Module 4Introduction à 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)
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).
Time-based One-Time Password (TOTP)
# 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 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
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/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
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 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
Stratégie Access Token / Refresh Token
// 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 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é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
L'authentification par API permet aux applications et scripts d'accéder à WikiJS de façon programmatique, avec un contrôle granulaire des permissions.
Gestion des clés API
# 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
-- 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);
# 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
// 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 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"
# 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
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.
Liens magiques (Magic Links)
# Configuration Magic Links
passwordless:
magicLinks:
enabled: true
# Génération des tokens
tokenLength: 32
algorithm: "crypto.randomBytes"
# Durée de validité
expiryMinutes: 15
# Sécurité
singleUse: true
verifyUserAgent: true
verifyIP: false # Peut causer des problèmes avec mobile
# Configuration email
emailTemplate:
subject: "Votre lien de connexion - {{siteName}}"
template: |
Bonjour,
Cliquez sur ce lien pour vous connecter :
{{magicLink}}
Ce lien expire dans {{expiryMinutes}} minutes.
Si vous n'avez pas demandé cette connexion, ignorez cet email.
# Rate limiting
rateLimiting:
maxAttempts: 3
windowMinutes: 60
# Post-connexion
redirectAfterLogin: "/dashboard"
createAccountIfNotExists: true
// Service Magic Links
class MagicLinkService {
constructor(options) {
this.redis = options.redis;
this.emailService = options.emailService;
this.tokenLength = options.tokenLength || 32;
this.expiryMinutes = options.expiryMinutes || 15;
}
async generateMagicLink(email) {
// Générer un token sécurisé
const token = crypto.randomBytes(this.tokenLength).toString('hex');
// Stocker en Redis avec expiration
const key = `magic_link:${token}`;
const data = {
email,
timestamp: Date.now(),
userAgent: req.get('user-agent'),
ip: req.ip
};
await this.redis.setex(
key,
this.expiryMinutes * 60,
JSON.stringify(data)
);
// Construire l'URL
const baseUrl = process.env.BASE_URL;
const magicLink = `${baseUrl}/auth/magic-link/${token}`;
return { token, magicLink };
}
async sendMagicLink(email, magicLink) {
const user = await User.findOne({ email });
if (!user && !this.createAccountIfNotExists) {
throw new Error('User not found');
}
await this.emailService.send({
to: email,
subject: 'Votre lien de connexion',
template: 'magic-link',
data: {
magicLink,
expiryMinutes: this.expiryMinutes,
siteName: process.env.SITE_NAME
}
});
}
async validateMagicLink(token, req) {
const key = `magic_link:${token}`;
const dataStr = await this.redis.get(key);
if (!dataStr) {
throw new Error('Invalid or expired magic link');
}
const data = JSON.parse(dataStr);
// Vérifications de sécurité
if (this.verifyUserAgent && data.userAgent !== req.get('user-agent')) {
throw new Error('User agent mismatch');
}
if (this.verifyIP && data.ip !== req.ip) {
throw new Error('IP address mismatch');
}
// Suppression du token (single use)
await this.redis.del(key);
return data.email;
}
}
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
// 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
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 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"