Passer au contenu principal

🔐 Qu'est-ce que OAuth 2.0 ?

📖 Définition

OAuth 2.0 est un protocole standard ouvert qui permet aux utilisateurs d'accorder l'accès à leurs informations à d'autres sites web ou applications sans partager leur mot de passe. Des exemples représentatifs sont les connexions sociales comme "Se connecter avec Google" ou "Se connecter avec GitHub". Il se concentre sur l'autorisation (Authorization) plutôt que sur l'authentification (Authentication), offrant une expérience utilisateur sécurisée et pratique.

🎯 Comprendre par analogies

Carte-clé d'hôtel

Méthode traditionnelle = Clé maître de l'hôtel
Propriétaire de l'hôtel : "Je vais vous donner la clé maître"
Vous : "Mais elle ouvre toutes les chambres ! 😱"
"C'est comme donner mon mot de passe"

OAuth 2.0 = Carte-clé d'hôtel
Propriétaire de l'hôtel : "Cette carte-clé..."
✅ Ouvre uniquement votre chambre
✅ Expire automatiquement au départ
✅ Si vous la perdez, la clé maître reste en sécurité
✅ Vous pouvez l'invalider à tout moment

Vous : "Parfait !"

Procuration

Scénario : Déléguer des affaires bancaires à un représentant

❌ Méthode de partage de mot de passe
Vous : "Mon mot de passe bancaire est 1234"
Représentant : "Je peux tout faire à la banque !"
Risque : Tous les droits comme retirer des fonds, supprimer des comptes, etc.

✅ Méthode OAuth
Vous : "Je vais à la banque et fais une procuration"
Banque : "Quelles autorisations voulez-vous accorder ?"
Vous : "Seulement consulter le solde"
Banque : "Pour combien de temps ?"
Vous : "Seulement une semaine"

Le représentant peut :
✅ Seulement consulter le solde
✅ Expire automatiquement après une semaine
✅ Ne connaît pas votre mot de passe
✅ L'autorisation peut être révoquée à tout moment

⚙️ Comment ça fonctionne

1. Concepts clés d'OAuth 2.0

┌─────────────────────────────────────────┐
│ 4 rôles OAuth 2.0 │
└─────────────────────────────────────────┘

1. Resource Owner (Propriétaire de ressource)
└─ Utilisateur (vous)

2. Client (Client)
└─ Application souhaitant utiliser le service
└─ Exemple : votre site web

3. Authorization Server (Serveur d'autorisation)
└─ Serveur accordant les autorisations
└─ Exemple : Google, GitHub, Naver

4. Resource Server (Serveur de ressources)
└─ Serveur contenant les informations de l'utilisateur
└─ Exemple : API Google, API GitHub

2. Flux d'authentification OAuth 2.0 (Authorization Code)

┌──────────┐                                 ┌──────────┐
│Utilisateur│ │ Client │
│ (vous) │ │ (webapp) │
└─────┬────┘ └────┬─────┘
│ │
│ 1. Clic sur "Se connecter avec Google" │
│──────────────────────────────────────────>│
│ │
│ 2. Redirection vers page de connexion Google│
│<──────────────────────────────────────────│
│ │
│ ┌──────────────────┐ │
│ │ Google │ │
│ 3. Connexion│(Serveur auth)│ │
│────────>│ │ │
│ │ │ │
│ 4. Demande d'autorisation │
│ "Cette app veut voir votre │
│ email et profil" │
│<────────│ │ │
│ │ │ │
│ 5. Approuver │
│────────>│ │ │
│ │ │ │
│ │ 6. Émettre Authorization Code │
│ │ (code à usage unique) │
│<────────│ │ │
│ └──────────────────┘ │
│ │
│ 7. Envoyer Authorization Code │
│──────────────────────────────────────────>│
│ │
│ ┌──────────────────┐ │
│ │ Google │ │
│ │ (Serveur auth) │ 8. Échange │
│ │ │<─── Code + │
│ │ │ Client │
│ │ │ Secret │
│ │ │ │
│ │ 9. Émettre Access Token ───────>│
│ │ │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ API Google │ │
│ │ (Serveur de │ 10. Demander│
│ │ ressources) │<──── info avec│
│ │ │ Token │
│ │ │ │
│ │ 11. Réponse info utilisateur ──>│
│ │ │ │
│ └──────────────────┘ │
│ │
│ 12. Connexion terminée ! │
│<──────────────────────────────────────────│
│ │

Points clés :
- Le mot de passe utilisateur n'est saisi que sur Google
- La webapp ne peut jamais connaître le mot de passe
- Accède uniquement aux informations autorisées avec Access Token

3. Types de Grant OAuth 2.0

========== 1. Authorization Code (le plus sûr) ==========
Usage : Applications web côté serveur
Avantages : Le plus sûr (utilise Client Secret)
Exemple : "Se connecter avec Google"

Flux :
1. Utilisateur → Connexion Google
2. Google → Émet Authorization Code
3. Serveur → Échange Code + Secret contre Access Token
4. Serveur → Appelle API avec Token

========== 2. Implicit (simple mais moins sûr) ==========
Usage : Apps navigateur uniquement (SPA)
Avantages : Simple (pas de serveur requis)
Inconvénients : Vulnérable (Token exposé dans URL)
Exemple : SPA anciennes

Flux :
1. Utilisateur → Connexion Google
2. Google → Émet Access Token directement (dans URL)
3. Navigateur → Appelle API avec Token
⚠️ Actuellement non recommandé

========== 3. Resource Owner Password (legacy) ==========
Usage : Applications de confiance uniquement
Avantages : Simple
Inconvénients : L'app connaît le mot de passe utilisateur
Exemple : App mobile propriétaire

Flux :
1. App → Reçoit ID/mot de passe utilisateur
2. App → Envoie à Authorization Server
3. Authorization Server → Émet Access Token
⚠️ Contraire à la philosophie OAuth, non recommandé

========== 4. Client Credentials ==========
Usage : Communication serveur à serveur (M2M)
Avantages : Pas d'authentification utilisateur requise
Exemple : Communication entre services backend

Flux :
1. Serveur → Envoie Client ID + Secret
2. Authorization Server → Émet Access Token
3. Serveur → Appelle API avec Token

========== 5. PKCE (Proof Key for Code Exchange) ==========
Usage : Apps mobiles, SPA (actuellement recommandé)
Avantages : Authorization Code + sécurité supplémentaire
Exemple : Apps mobiles/SPA modernes

Flux :
1. App → Génère code_verifier (chaîne aléatoire)
2. App → Génère code_challenge (hash du verifier)
3. App → Demande Authorization Code avec challenge
4. Google → Émet Code
5. App → Échange Code + verifier contre Token
6. Google → Vérifie verifier et émet Token

💡 Exemples réels

Connexion Google OAuth 2.0 (Node.js)

// ========== 1. Configuration de Google Cloud Console ==========
/*
1. Accéder à https://console.cloud.google.com
2. Créer un projet
3. "API et services" → "Identifiants"
4. Créer "ID client OAuth 2.0"
5. Ajouter URI de redirection autorisée :
http://localhost:3000/auth/google/callback
6. Obtenir Client ID et Client Secret
*/

// ========== 2. Implémentation du serveur ==========
const express = require('express');
const axios = require('axios');
const session = require('express-session');

const app = express();

// Configuration de la session
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: true
}));

// Configuration OAuth Google
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/google/callback';

// ========== Étape 1 : Démarrer la connexion ==========
app.get('/auth/google', (req, res) => {
// Générer l'URL d'authentification Google
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';

const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code', // Demander Authorization Code
scope: 'openid email profile', // Autorisations à demander
access_type: 'offline', // Recevoir Refresh Token
prompt: 'consent' // Toujours afficher l'écran de consentement
});

// Rediriger vers la page de connexion Google
res.redirect(`${authUrl}?${params}`);
});

// ========== Étape 2 : Traiter le callback ==========
app.get('/auth/google/callback', async (req, res) => {
const { code, error } = req.query;

// Si l'utilisateur refuse
if (error) {
return res.status(400).send(`Authentification échouée : ${error}`);
}

try {
// Échanger Authorization Code contre Access Token
const tokenResponse = await axios.post(
'https://oauth2.googleapis.com/token',
{
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
}
);

const {
access_token,
refresh_token,
expires_in,
token_type
} = tokenResponse.data;

console.log('Access Token:', access_token);
console.log('Expire dans :', expires_in, 'secondes');

// Obtenir les informations utilisateur avec Access Token
const userResponse = await axios.get(
'https://www.googleapis.com/oauth2/v2/userinfo',
{
headers: {
Authorization: `Bearer ${access_token}`
}
}
);

const user = userResponse.data;
/*
{
id: "123456789",
email: "user@gmail.com",
verified_email: true,
name: "Jean Dupont",
picture: "https://lh3.googleusercontent.com/...",
locale: "fr"
}
*/

// Sauvegarder les informations utilisateur en session
req.session.user = {
id: user.id,
email: user.email,
name: user.name,
picture: user.picture,
accessToken: access_token,
refreshToken: refresh_token
};

// Sauvegarder l'utilisateur en base de données
await saveOrUpdateUser(user);

// Rediriger vers la page principale
res.redirect('/dashboard');
} catch (error) {
console.error('Erreur OAuth:', error.response?.data || error.message);
res.status(500).send('Erreur lors du traitement de l\'authentification');
}
});

// ========== Vérifier les informations utilisateur ==========
app.get('/dashboard', (req, res) => {
if (!req.session.user) {
return res.redirect('/');
}

res.send(`
<h1>Bonjour, ${req.session.user.name} !</h1>
<img src="${req.session.user.picture}" alt="Profil" />
<p>Email : ${req.session.user.email}</p>
<a href="/logout">Se déconnecter</a>
`);
});

// ========== Déconnexion ==========
app.get('/logout', async (req, res) => {
// Révoquer Access Token Google (optionnel)
const accessToken = req.session.user?.accessToken;
if (accessToken) {
try {
await axios.post(
`https://oauth2.googleapis.com/revoke?token=${accessToken}`
);
} catch (error) {
console.error('Échec de la révocation du token:', error.message);
}
}

// Supprimer la session
req.session.destroy();
res.redirect('/');
});

// ========== Page d'accueil ==========
app.get('/', (req, res) => {
res.send(`
<h1>Démo OAuth 2.0</h1>
<a href="/auth/google">
<button>Se connecter avec Google</button>
</a>
`);
});

app.listen(3000, () => {
console.log('Serveur en cours d\'exécution : http://localhost:3000');
});

Implémentation facile avec Passport.js

// ========== Utiliser Passport.js ==========
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

// Configuration de Passport
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
// Traiter les informations utilisateur
try {
// Chercher ou créer l'utilisateur en base de données
let user = await User.findOne({ googleId: profile.id });

if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
picture: profile.photos[0].value
});
}

done(null, user);
} catch (error) {
done(error, null);
}
}
));

// Sérialisation de session
passport.serializeUser((user, done) => {
done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
const user = await User.findById(id);
done(null, user);
});

// Configuration Express
app.use(passport.initialize());
app.use(passport.session());

// ========== Routes ==========

// Démarrer connexion Google
app.get('/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);

// Callback Google
app.get('/auth/google/callback',
passport.authenticate('google', {
failureRedirect: '/login',
successRedirect: '/dashboard'
})
);

// Déconnexion
app.get('/logout', (req, res) => {
req.logout((err) => {
if (err) console.error(err);
res.redirect('/');
});
});

// Middleware d'authentification
function isAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}

// Route protégée
app.get('/dashboard', isAuthenticated, (req, res) => {
res.send(`Bonjour, ${req.user.name} !`);
});

GitHub OAuth 2.0

// ========== Configuration GitHub OAuth ==========
/*
1. Accéder à https://github.com/settings/developers
2. Cliquer sur "New OAuth App"
3. Entrer les informations :
- Application name: My App
- Homepage URL: http://localhost:3000
- Authorization callback URL: http://localhost:3000/auth/github/callback
4. Obtenir Client ID et Client Secret
*/

const express = require('express');
const axios = require('axios');
const app = express();

const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/github/callback';

// ========== Connexion GitHub ==========
app.get('/auth/github', (req, res) => {
const authUrl = 'https://github.com/login/oauth/authorize';

const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'user:email', // Autorisations à demander
state: Math.random().toString(36) // Protection CSRF
});

res.redirect(`${authUrl}?${params}`);
});

// ========== Traiter le callback ==========
app.get('/auth/github/callback', async (req, res) => {
const { code, state } = req.query;

try {
// 1. Obtenir Access Token
const tokenResponse = await axios.post(
'https://github.com/login/oauth/access_token',
{
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
code,
redirect_uri: REDIRECT_URI
},
{
headers: { Accept: 'application/json' }
}
);

const { access_token } = tokenResponse.data;

// 2. Obtenir les informations utilisateur
const userResponse = await axios.get('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${access_token}`
}
});

const user = userResponse.data;
/*
{
login: "username",
id: 12345,
avatar_url: "https://avatars.githubusercontent.com/...",
name: "Jean Dupont",
email: "user@example.com",
bio: "Je suis développeur",
public_repos: 50
}
*/

// 3. Obtenir l'email (requête séparée)
const emailResponse = await axios.get('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${access_token}`
}
});

const primaryEmail = emailResponse.data.find(email => email.primary);

res.json({
user,
email: primaryEmail.email
});
} catch (error) {
console.error('Erreur OAuth GitHub:', error.message);
res.status(500).send('Authentification échouée');
}
});

app.listen(3000);

Utiliser OAuth dans React

// ========== Client React ==========

import React, { useEffect, useState } from 'react';
import axios from 'axios';

function App() {
const [user, setUser] = useState(null);

// Vérifier les informations utilisateur au chargement de la page
useEffect(() => {
checkAuth();
}, []);

async function checkAuth() {
try {
const response = await axios.get('/api/me', {
withCredentials: true // Inclure les cookies
});
setUser(response.data);
} catch (error) {
console.log('Non connecté');
}
}

function handleGoogleLogin() {
// Aller au point de terminaison OAuth du serveur
window.location.href = 'http://localhost:3000/auth/google';
}

function handleGitHubLogin() {
window.location.href = 'http://localhost:3000/auth/github';
}

function handleLogout() {
window.location.href = 'http://localhost:3000/logout';
}

if (user) {
return (
<div>
<h1>Bonjour, {user.name} !</h1>
<img src={user.picture} alt="Profil" />
<p>{user.email}</p>
<button onClick={handleLogout}>Se déconnecter</button>
</div>
);
}

return (
<div>
<h1>Se connecter</h1>
<button onClick={handleGoogleLogin}>
🔵 Se connecter avec Google
</button>
<button onClick={handleGitHubLogin}>
Se connecter avec GitHub
</button>
</div>
);
}

export default App;

Améliorer la sécurité avec PKCE (SPA/Mobile)

// ========== PKCE (Proof Key for Code Exchange) ==========
// Pour les apps mobiles ou SPA

const crypto = require('crypto');

// ========== 1. Générer Code Verifier ==========
function generateCodeVerifier() {
return base64URLEncode(crypto.randomBytes(32));
}

// ========== 2. Générer Code Challenge ==========
function generateCodeChallenge(verifier) {
const hash = crypto.createHash('sha256')
.update(verifier)
.digest();
return base64URLEncode(hash);
}

function base64URLEncode(buffer) {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}

// ========== 3. Démarrer connexion PKCE (client) ==========
async function startPKCELogin() {
// Générer et sauvegarder Verifier
const codeVerifier = generateCodeVerifier();
sessionStorage.setItem('code_verifier', codeVerifier);

// Générer Challenge
const codeChallenge = generateCodeChallenge(codeVerifier);

// URL d'authentification Google
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'openid email profile',
code_challenge: codeChallenge, // PKCE
code_challenge_method: 'S256' // SHA-256
});

window.location.href = `${authUrl}?${params}`;
}

// ========== 4. Traiter le callback ==========
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');

// Récupérer le Verifier sauvegardé
const codeVerifier = sessionStorage.getItem('code_verifier');

// Échanger Token
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier // PKCE Verifier
})
});

const { access_token } = await response.json();

// Supprimer Verifier
sessionStorage.removeItem('code_verifier');

return access_token;
}

🤔 Questions fréquentes

Q1. OAuth vs JWT vs Session ?

R :

// ========== 1. Session (Session) ==========
// Méthode traditionnelle

// Connexion
app.post('/login', (req, res) => {
const { username, password } = req.body;

// Vérifier l'authentification
if (isValidUser(username, password)) {
// Sauvegarder en session
req.session.userId = user.id;
res.json({ message: 'Connexion réussie' });
}
});

// Vérifier l'authentification
app.get('/profile', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Connexion requise' });
}

const user = getUserById(req.session.userId);
res.json(user);
});

Avantages : Simple, contrôle total du serveur
Inconvénients : Requiert mémoire/stockage serveur, faible évolutivité

// ========== 2. JWT (JSON Web Token) ==========
// Authentification Stateless

// Connexion
app.post('/login', (req, res) => {
const { username, password } = req.body;

if (isValidUser(username, password)) {
// Générer JWT
const token = jwt.sign(
{ userId: user.id, username: user.username },
SECRET_KEY,
{ expiresIn: '1h' }
);

res.json({ token });
}
});

// Vérifier l'authentification
app.get('/profile', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];

try {
const decoded = jwt.verify(token, SECRET_KEY);
const user = getUserById(decoded.userId);
res.json(user);
} catch (error) {
res.status(401).json({ error: 'Token invalide' });
}
});

Avantages : Stateless, bonne évolutivité
Inconvénients : Difficile à invalider, grande taille

// ========== 3. OAuth 2.0 ==========
// Authentification et autorisation tierce

// Utilisateur se connecte avec Google
// → Google émet Access Token
// → Notre app appelle API Google avec Token

Avantages :
- Pas de partage de mot de passe
- Contrôle granulaire des autorisations
- Protocole standard
- Connexion sociale possible

Inconvénients :
- Complexe
- Dépendance au service externe

// ========== Comparaison ==========
Caractéristique| Session | JWT | OAuth
---------------|---------|----------|-------
Emplacement | Serveur | Client | Serveur
Évolutivité | Faible | Élevée | Élevée
Sécurité | Élevée | Moyenne | Élevée
Complexité | Faible | Moyenne | Élevée
Auth tierce | Non | Non | Oui

// ========== Usage combiné ==========
// Émettre JWT après connexion OAuth

app.get('/auth/google/callback', async (req, res) => {
// 1. Authentifier l'utilisateur avec Google OAuth
const googleUser = await getGoogleUser(req.query.code);

// 2. Sauvegarder l'utilisateur dans notre base de données
const user = await saveOrUpdateUser(googleUser);

// 3. Générer et renvoyer JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
SECRET_KEY,
{ expiresIn: '7d' }
);

res.json({ token });
});

// Les requêtes suivantes s'authentifient avec JWT
app.get('/api/profile', authenticateJWT, (req, res) => {
res.json(req.user);
});

Q2. Quelle est la différence entre Access Token et Refresh Token ?

R :

// ========== Access Token ==========
// Durée de vie courte (15 minutes~1 heure)
// Utilisé dans les requêtes API

// ========== Refresh Token ==========
// Durée de vie longue (7 jours~30 jours)
// Utilisé pour renouveler l'Access Token

// ========== Exemple d'implémentation ==========

// Émettre les deux lors de la connexion
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body);

// Access Token (15 minutes)
const accessToken = jwt.sign(
{ userId: user.id, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);

// Refresh Token (7 jours)
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);

// Sauvegarder Refresh Token en DB (pour invalidation)
await saveRefreshToken(user.id, refreshToken);

res.json({ accessToken, refreshToken });
});

// ========== Requête API (Access Token) ==========
app.get('/api/data', authenticateAccess, (req, res) => {
res.json({ data: 'Données importantes' });
});

function authenticateAccess(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];

try {
const decoded = jwt.verify(token, ACCESS_SECRET);

if (decoded.type !== 'access') {
throw new Error('Type de token incorrect');
}

req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'access_token_expired',
message: 'Renouveler avec Refresh Token'
});
}

res.status(401).json({ error: 'Token invalide' });
}
}

// ========== Renouveler Access Token ==========
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;

if (!refreshToken) {
return res.status(401).json({ error: 'Refresh Token requis' });
}

try {
// 1. Vérifier Refresh Token
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);

if (decoded.type !== 'refresh') {
throw new Error('Type de token incorrect');
}

// 2. Vérifier en DB (si invalidé)
const isValid = await isRefreshTokenValid(decoded.userId, refreshToken);
if (!isValid) {
throw new Error('Refresh Token invalidé');
}

// 3. Émettre nouveau Access Token
const newAccessToken = jwt.sign(
{ userId: decoded.userId, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);

res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(401).json({ error: 'Refresh Token invalide' });
}
});

// ========== Renouvellement automatique en frontend ==========
import axios from 'axios';

let accessToken = localStorage.getItem('accessToken');
let refreshToken = localStorage.getItem('refreshToken');

// Intercepteur Axios
axios.interceptors.request.use(
config => {
config.headers.Authorization = `Bearer ${accessToken}`;
return config;
}
);

axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;

// Access Token expiré
if (error.response?.data?.error === 'access_token_expired' &&
!originalRequest._retry) {
originalRequest._retry = true;

try {
// Renouveler avec Refresh Token
const response = await axios.post('/auth/refresh', {
refreshToken
});

accessToken = response.data.accessToken;
localStorage.setItem('accessToken', accessToken);

// Réessayer la requête originale
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh Token aussi expiré → Déconnexion
localStorage.clear();
window.location.href = '/login';
}
}

return Promise.reject(error);
}
);

// ========== Conseils de sécurité ==========

// ✅ Bonnes pratiques
1. Access Token : Durée de vie courte (15 minutes)
2. Refresh Token : Sauvegarder en DB pour invalidation
3. Refresh Token dans cookie HttpOnly
4. HTTPS obligatoire
5. Refresh Token Rotation (émettre nouveau token lors du renouvellement)

// ❌ Mauvaises pratiques
1. Durée de vie Access Token trop longue
2. Sauvegarder Refresh Token dans localStorage
3. Refresh Token non invalidable
4. Utiliser HTTP

Q3. Qu'est-ce que Scope (portée des autorisations) ?

R :

// ========== Concept de Scope ==========
// Portée des autorisations que l'application demande

// ========== Scopes Google OAuth ==========
const scopes = [
'openid', // Informations de base
'email', // Email
'profile', // Profil (nom, photo)
'https://www.googleapis.com/auth/drive.readonly', // Lecture Drive
'https://www.googleapis.com/auth/gmail.send' // Envoi Gmail
];

// Demander des autorisations lors de la connexion
app.get('/auth/google', (req, res) => {
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: scopes.join(' ') // Séparer par espace
});

res.redirect(`${authUrl}?${params}`);
});

// Écran affiché à l'utilisateur :
/*
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
My App demande :

✓ Voir l'adresse email
✓ Voir les informations de profil de base
✓ Voir les fichiers Google Drive
✓ Envoyer des emails Gmail

[Autoriser] [Refuser]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
*/

// ========== Scopes GitHub OAuth ==========
const githubScopes = [
'user', // Informations utilisateur
'user:email', // Email (incluant privé)
'repo', // Accès complet aux dépôts
'public_repo', // Dépôts publics uniquement
'read:org', // Lire informations organisation
'write:org' // Écrire informations organisation
];

app.get('/auth/github', (req, res) => {
const authUrl = 'https://github.com/login/oauth/authorize';
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'user:email public_repo' // Seulement autorisations nécessaires
});

res.redirect(`${authUrl}?${params}`);
});

// ========== Principe du moindre privilège ==========

// ❌ Mauvaise pratique : Demander plus d'autorisations que nécessaire
scope: 'user repo admin:org' // Trop d'autorisations !

// ✅ Bonne pratique : Seulement autorisations nécessaires
scope: 'user:email' // Seulement email nécessaire

// ========== Demande de Scope dynamique ==========

// Connexion initiale : Seulement informations de base
app.get('/auth/google', (req, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'email profile' // Seulement informations de base
});

res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});

// Quand des autorisations supplémentaires sont nécessaires plus tard
app.get('/auth/google/drive', (req, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'https://www.googleapis.com/auth/drive.readonly',
prompt: 'consent' // Demander à nouveau le consentement
});

res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});

// ========== Autorisation incrémentale ==========
// Ajouter des autorisations progressivement quand nécessaire

// Étape 1 : Connexion (informations de base)
scope: 'email profile'

// Étape 2 : Intégration Google Drive (quand l'utilisateur le souhaite)
scope: 'https://www.googleapis.com/auth/drive'

// Étape 3 : Intégration Gmail (quand l'utilisateur le souhaite)
scope: 'https://www.googleapis.com/auth/gmail.send'

// ========== Vérification de Scope ==========

// Vérifier le scope inclus dans Access Token
async function checkScope(accessToken, requiredScope) {
const response = await axios.get(
'https://www.googleapis.com/oauth2/v1/tokeninfo',
{ params: { access_token: accessToken } }
);

const grantedScopes = response.data.scope.split(' ');

if (!grantedScopes.includes(requiredScope)) {
throw new Error('Autorisations insuffisantes');
}
}

// Exemple d'utilisation
app.get('/api/drive/files', async (req, res) => {
const accessToken = req.headers.authorization?.split(' ')[1];

try {
// Vérifier l'autorisation Drive
await checkScope(accessToken, 'https://www.googleapis.com/auth/drive.readonly');

// Appeler API Drive
const files = await listDriveFiles(accessToken);
res.json(files);
} catch (error) {
res.status(403).json({
error: 'Autorisations insuffisantes',
message: 'Accès Google Drive requis'
});
}
});

Q4. Quelles sont les meilleures pratiques de sécurité OAuth ?

R :

// ========== 1. HTTPS obligatoire ==========
// ✅ Toujours utiliser HTTPS
const REDIRECT_URI = 'https://myapp.com/callback';

// ❌ HTTP absolument interdit
const REDIRECT_URI = 'http://myapp.com/callback'; // Risque de vol de token !

// ========== 2. Paramètre State (Protection CSRF) ==========

// Démarrer la connexion
app.get('/auth/google', (req, res) => {
// Générer et sauvegarder state aléatoire
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;

const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'email profile',
state // Protection CSRF
});

res.redirect(`${authUrl}?${params}`);
});

// Vérifier dans le callback
app.get('/auth/google/callback', (req, res) => {
const { state, code } = req.query;

// Vérifier state
if (state !== req.session.oauthState) {
return res.status(403).send('Attaque CSRF détectée');
}

// Supprimer state
delete req.session.oauthState;

// ... continuer le traitement
});

// ========== 3. Protéger Client Secret ==========

// ✅ Utiliser variables d'environnement
const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;

// ✅ Utiliser uniquement sur le serveur
// Ne jamais exposer dans le frontend !

// ❌ Ne pas hardcoder dans le code
const CLIENT_SECRET = 'abc123xyz'; // Absolument interdit !

// ========== 4. Valider Redirect URI ==========

// Autoriser uniquement les URIs enregistrées dans Google Cloud Console
// ✅ URI correspondant exactement
https://myapp.com/auth/callback

// ❌ Ne pas utiliser de wildcards
https://myapp.com/*

// ❌ Vulnérabilité de redirection ouverte
https://myapp.com/callback?redirect=evil.com

// ========== 5. Stockage de Token ==========

// ✅ Serveur : Variables d'environnement ou DB chiffrée
await saveToken(userId, encryptToken(accessToken));

// ✅ Client : Cookie HttpOnly (Protection XSS)
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true, // HTTPS uniquement
sameSite: 'strict',
maxAge: 3600000 // 1 heure
});

// ❌ localStorage (vulnérable au XSS)
localStorage.setItem('token', accessToken); // Dangereux !

// ========== 6. Durée d'expiration du Token ==========

// ✅ Durée de vie courte
const accessToken = jwt.sign(payload, secret, {
expiresIn: '15m' // 15 minutes
});

// ✅ Utiliser Refresh Token
const refreshToken = jwt.sign(payload, secret, {
expiresIn: '7d' // 7 jours
});

// ❌ Pas de durée d'expiration
const token = jwt.sign(payload, secret); // Dangereux !

// ========== 7. Minimiser Scope ==========

// ✅ Seulement autorisations nécessaires
scope: 'email profile'

// ❌ Demander toutes les autorisations
scope: 'https://www.googleapis.com/auth/drive' // Inutile

// ========== 8. Implémenter révocation de Token ==========

// Révoquer Token lors de la déconnexion
app.get('/logout', async (req, res) => {
const accessToken = req.cookies.access_token;

// Révoquer Token Google
await axios.post(
`https://oauth2.googleapis.com/revoke?token=${accessToken}`
);

// Supprimer Refresh Token de la DB
await deleteRefreshToken(req.user.id);

// Supprimer cookie
res.clearCookie('access_token');

res.redirect('/');
});

// ========== 9. Utiliser PKCE (Mobile/SPA) ==========

// Améliorer la sécurité sans Client Secret
const codeVerifier = generateRandomString(128);
const codeChallenge = base64URLEncode(sha256(codeVerifier));

// Envoyer challenge dans la demande d'authentification
const params = {
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
code_challenge: codeChallenge,
code_challenge_method: 'S256'
};

// Envoyer verifier lors de l'échange de Token
const tokenParams = {
code,
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier // Au lieu de Secret
};

// ========== 10. Renouvellement régulier de Token ==========

// Renouvellement automatique en arrière-plan
setInterval(async () => {
try {
const newAccessToken = await refreshAccessToken(refreshToken);
updateToken(newAccessToken);
} catch (error) {
console.error('Échec du renouvellement de token:', error);
redirectToLogin();
}
}, 14 * 60 * 1000); // Toutes les 14 minutes (avant expiration à 15 min.)

Q5. Authentification (Authentication) vs Autorisation (Authorization) ?

R :

// ========== Authentification (Authentication) ==========
// "Qui êtes-vous ?" - Vérification d'identité

// Exemple : Connexion
const user = await authenticateUser(username, password);
if (user) {
console.log('Vous êtes Jean Dupont ✓');
}

// ========== Autorisation (Authorization) ==========
// "Que pouvez-vous faire ?" - Vérification d'autorisations

// Exemple : Accès administrateur uniquement
if (user.role === 'admin') {
console.log('Fonctions administrateur disponibles ✓');
} else {
console.log('Pas d\'autorisation ✗');
}

// ========== Rôle de OAuth 2.0 ==========

// OAuth est principalement Authorization (autorisation)
// - L'utilisateur accorde des autorisations à l'app
// - "Autoriser cette app à voir mon Google Drive"

// Mais aussi utilisé pour l'authentification (Authentication)
// - OpenID Connect (OAuth 2.0 + authentification)
// - "Se connecter avec Google"

// ========== Exemple réel ==========

// Étape 1 : Authentification (Authentication)
app.post('/login', async (req, res) => {
const { username, password } = req.body;

// Vérifier l'identité utilisateur
const user = await User.findOne({ username });

if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Authentification échouée' });
}

// Authentification réussie → Émettre token
const token = jwt.sign(
{
userId: user.id,
role: user.role // Inclure informations d'autorisations
},
SECRET_KEY
);

res.json({ token });
});

// Étape 2 : Autorisation (Authorization)
app.get('/admin/users', authenticateToken, authorizeAdmin, (req, res) => {
// Seuls les administrateurs peuvent accéder
const users = await User.findAll();
res.json(users);
});

function authenticateToken(req, res, next) {
// Authentification : Vérifier token
const token = req.headers.authorization?.split(' ')[1];

try {
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Authentification requise' });
}
}

function authorizeAdmin(req, res, next) {
// Autorisation : Vérifier administrateur
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Pas d\'autorisation' });
}
next();
}

// ========== Authentification + Autorisation avec OAuth ==========

// 1. Authentification (OpenID Connect)
app.get('/auth/google/callback', async (req, res) => {
// Vérifier l'utilisateur avec Google OAuth
const googleUser = await getGoogleUser(req.query.code);

// Authentification terminée → Identité vérifiée
console.log('Utilisateur:', googleUser.email);

// Se connecter à notre système
const user = await findOrCreateUser(googleUser);
req.session.userId = user.id;
});

// 2. Autorisation (Scope)
app.get('/api/drive/files', async (req, res) => {
// Vérifier l'autorisation Google Drive
const hasPermission = await checkGoogleScope(
req.user.accessToken,
'https://www.googleapis.com/auth/drive.readonly'
);

if (!hasPermission) {
return res.status(403).json({
error: 'Autorisation requise',
message: 'Veuillez accorder l\'accès à Google Drive'
});
}

// A l'autorisation → Appeler API
const files = await listDriveFiles(req.user.accessToken);
res.json(files);
});

// ========== Résumé comparatif ==========

Authentification (Authentication)
- Question : "Qui êtes-vous ?"
- Objectif : Vérifier l'identité
- Exemples :
* Saisir mot de passe
* Reconnaissance d'empreinte
* Se connecter avec Google
- Code HTTP : 401 Unauthorized

Autorisation (Authorization)
- Question : "Que pouvez-vous faire ?"
- Objectif : Vérifier les autorisations d'accès
- Exemples :
* Droits administrateur
* Autorisation de lecture Google Drive
* OAuth Scope
- Code HTTP : 403 Forbidden

// Les deux sont nécessaires !
// 1. D'abord l'authentification (connexion)
// 2. Puis vérifier les autorisations (permettre l'accès)

🎓 Prochaines étapes

Si vous avez compris OAuth 2.0, essayez d'apprendre ce qui suit :

  1. Token JWT (document en préparation) - Authentification basée sur les tokens
  2. Qu'est-ce que HTTPS ? (document en préparation) - Communication sécurisée
  3. Qu'est-ce que CORS ? (document en préparation) - Sécurité API

Pratique

# ========== 1. Pratique Google OAuth ==========

# Créer projet
mkdir oauth-demo
cd oauth-demo
npm init -y

# Installer packages
npm install express axios express-session dotenv

# Créer fichier .env
cat > .env << EOF
GOOGLE_CLIENT_ID=votre_client_id
GOOGLE_CLIENT_SECRET=votre_client_secret
SESSION_SECRET=votre_session_secret
EOF

# Écrire server.js et exécuter
node server.js

# Accéder à http://localhost:3000

# ========== 2. Pratique Passport.js ==========

npm install passport passport-google-oauth20

# ========== 3. Implémenter plusieurs connexions sociales ==========

npm install passport-github2 passport-facebook

# Supporter Google, GitHub, Facebook

🎬 Conclusion

OAuth 2.0 est le protocole d'authentification standard du web moderne :

  • Sécurité : Authentification sécurisée sans partager les mots de passe
  • Commodité : Inscription rapide avec connexion sociale
  • Contrôle des autorisations : Accorder uniquement les autorisations nécessaires
  • Standard : Supporté par tous les principaux services

Construisez un système d'authentification sécurisé et pratique ! 🔐