11. Socket Auth
Sécurisez vos connexions Socket.IO avec l'authentification et les middleware
Introduction
L'authentification est cruciale pour sécuriser vos applications temps réel. Sans elle, n'importe qui pourrait se connecter à votre serveur et accéder à des données sensibles.
Dans ce chapitre, nous allons étendre notre classe ChatServer pour ajouter l'authentification JWT. Nous
verrons comment :
- Utiliser les middlewares Socket.IO pour valider les connexions
- Implémenter l'authentification avec JWT
- Stocker et utiliser les données utilisateur authentifiées
- Gérer les permissions (rôles admin/user)
Middlewares Socket.IO
Les middlewares s'exécutent avant l'établissement d'une connexion. Ils permettent de valider ou rejeter une connexion.
io.use((socket, next) => {
if (isValid) {
next() // Autoriser
} else {
next(new Error('Authentification échouée')) // Rejeter
}
})
Adaptation de ChatServer
Dans le chapitre 7. Authentification, nous avons déjà mis en place JWT pour
Express avec le middleware
authenticateToken. Nous allons réutiliser cette logique pour Socket.IO.
import {Server as HTTPServer} from 'http'
import {Server, Socket} from 'socket.io'
import jwt from 'jsonwebtoken'
// Types simplifiés pour les événements
interface ClientToServerEvents {
user: (username: string) => void
message: (username: string, message: string) => void
}
interface ServerToClientEvents {
welcome: (message: string) => void
'user-joined': (message: string) => void
message: (data: { username: string, message: string }) => void
}
// Données stockées après authentification (correspond au JWT du cours 11)
interface UserData {
userId: number
email: string
}
type TypedSocket = Socket<ClientToServerEvents, ServerToClientEvents>
type TypedServer = Server<ClientToServerEvents, ServerToClientEvents>
export class ChatServer {
private io: TypedServer
constructor(httpServer: HTTPServer) {
this.io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer, {
cors: {origin: '*'},
})
this.setupAuthMiddleware() // Nouveau : ajout de l'authentification
this.initializeSocket()
}
// Nouveau : middleware d'authentification
private setupAuthMiddleware() {
this.io.use((socket, next) => {
const token = socket.handshake.auth.token
if (!token) {
return next(new Error('Token manquant'))
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as UserData
socket.data = decoded
next()
} catch (error) {
next(new Error('Token invalide ou expiré'))
}
})
}
private initializeSocket() {
this.io.on('connection', (socket) => {
// Récupérer les données de l'utilisateur authentifié
const userData = socket.data as UserData
console.log('Nouvelle connexion:', socket.id, `(${userData.email})`)
socket.emit('welcome', `Bienvenue ${userData.email}!`)
socket.on('user', (username) => this.handleUser(socket, userData))
socket.on('message', (username, message) => this.handleMessage(socket, userData, message))
})
}
private handleUser(socket: TypedSocket, userData: UserData) {
console.log('Utilisateur connecté:', userData.email)
socket.broadcast.emit('user-joined', `${userData.email} s'est connecté`)
}
private handleMessage(socket: TypedSocket, userData: UserData, message: string) {
console.log(`${userData.email}: ${message}`)
this.io.emit('message', {username: userData.email, message})
}
}
Explication des modifications
Voici ce qui a été ajouté :
- Import de
jsonwebtoken: Pour vérifier les tokens JWT - Interface
UserData: Définit la structure des données stockées danssocket.dataaprès authentification - Méthode
setupAuthMiddleware():- Récupère le token depuis
socket.handshake.auth.token - Vérifie le token avec
jwt.verify()(même approche que le middleware Express) - Stocke les données décodées dans
socket.data - Rejette la connexion si le token est invalide
- Récupère le token depuis
- Modification de
initializeSocket(): Récupère les données utilisateur depuissocket.data - Modification des handlers : Utilisent
userDataau lieu duusernamepassé en paramètre
Client HTML
Créez public/chat-auth.html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Chat avec Authentification</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
h1 {
margin-bottom: 20px;
}
.hidden {
display: none;
}
#loginForm input {
display: block;
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
}
#messages {
border: 1px solid #ddd;
height: 400px;
overflow-y: auto;
padding: 10px;
margin: 10px 0;
background: #f9f9f9;
}
#messages div {
margin: 5px 0;
padding: 5px;
}
.system {
color: #666;
font-style: italic;
}
.error {
color: red;
}
#form {
display: flex;
gap: 10px;
}
input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
}
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
</head>
<body>
<h1>Chat Socket.IO avec Auth</h1>
<div id="loginForm">
<input id="email" placeholder="Email" type="email"/>
<input id="password" placeholder="Password" type="password"/>
<button id="loginButton">Se connecter</button>
<p class="error" id="loginError"></p>
<p><small>Comptes : alice@example.com/password123 ou bob@example.com/password123</small></p>
</div>
<div class="hidden" id="chatContainer">
<p>Connecté : <strong id="currentUser"></strong></p>
<div id="messages"></div>
<form id="form">
<input autocomplete="off" id="input" placeholder="Votre message..."/>
<button>Envoyer</button>
</form>
</div>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>
let socket, userEmail
document.getElementById('loginButton').addEventListener('click', async () => {
const email = document.getElementById('email').value.trim()
const password = document.getElementById('password').value.trim()
const loginError = document.getElementById('loginError')
loginError.textContent = ''
try {
const response = await fetch('/auth/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email, password})
})
if (!response.ok) {
const error = await response.json()
loginError.textContent = error.error
return
}
const {token} = await response.json()
userEmail = email
document.getElementById('loginForm').classList.add('hidden')
document.getElementById('chatContainer').classList.remove('hidden')
document.getElementById('currentUser').textContent = userEmail
connectToChat(token)
} catch (error) {
loginError.textContent = 'Erreur réseau'
}
})
function connectToChat(token) {
socket = io('http://localhost:3000', {auth: {token}})
const messages = document.getElementById('messages')
const form = document.getElementById('form')
const input = document.getElementById('input')
socket.emit('user', userEmail)
function addMessage(text, isSystem = false) {
const div = document.createElement('div')
div.className = isSystem ? 'system' : ''
div.textContent = text
messages.appendChild(div)
messages.scrollTop = messages.scrollHeight
}
socket.on('welcome', (msg) => addMessage(msg, true))
socket.on('user-joined', (msg) => addMessage(msg, true))
socket.on('message', (data) => addMessage(`${data.username}: ${data.message}`))
form.addEventListener('submit', (e) => {
e.preventDefault()
if (input.value) {
socket.emit('message', userEmail, input.value)
input.value = ''
}
})
}
</script>
</body>
</html>
Servir le fichier HTML
Lorsque vous allez envoyer la requête POST /auth/login, le navigateur va envoyer une erreur. En effet le fichier
chat-auth.html n'est pas servi par le serveur Node mais par le système de fichiers local. Il faut donc modifier le
serveur
pour qu'il serve ce fichier.
import 'dotenv/config'
import express from 'express'
import {userRouter} from "@/routes/user.route";
import {authRouter} from "@/routes/auth.route";
import swaggerUi from 'swagger-ui-express'
import {swaggerDocument} from './docs'
import * as http from "node:http";
import {ChatServer} from "@/socket/Chat";
const app = express()
const server = http.createServer(app);
// Initialiser le serveur de chat avec la classe
new ChatServer(server)
const port = 3000
// Middleware pour parser le JSON
app.use(express.json())
// Sert les fichiers statiques
app.use(express.static('public'))
// Documentation Swagger UI
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: "API Documentation"
}))
// Route Express classique
app.get('/', (_req, res) => {
res.send('Serveur Socket.IO actif')
})
// Autres middlewares et routes Express
app.use('/users', userRouter)
app.use('/auth', authRouter)
server.listen(port, () => {
console.log(`Serveur démarré sur http://localhost:${port}`)
})
On a ajouté
express.static('public')pour servir les fichiers statiques dans le dossierpublic.