Maîtriser l'Authentification et l'Autorisation pour les Applications Web Modernes
Maîtriser l'Authentification et l'Autorisation pour les Applications Web Modernes

Explorer OpenID Connect pour l'Authentification

Introduction : L'Évolution de l'Authentification Web

Dans le monde des applications web modernes, l'authentification et l'autorisation sont des piliers fondamentaux. Autrefois, l'authentification se résumait souvent à un simple formulaire de connexion avec nom d'utilisateur et mot de passe. Cependant, avec l'avènement des applications distribuées, des APIs et des microservices, le besoin de méthodes d'authentification plus flexibles, sécurisées et interopérables est devenu crucial.

C'est dans ce contexte qu'OAuth 2.0 a émergé comme un standard de délégation d'autorisation, permettant à une application d'accéder aux ressources d'un utilisateur sur un serveur tiers sans jamais connaître ses identifiants. Mais OAuth 2.0, bien qu'excellent pour l'autorisation, n'est pas conçu pour l'authentification. Il ne fournit pas d'informations standardisées sur l'identité de l'utilisateur qui a accordé l'accès.

C'est là qu'OpenID Connect (OIDC) intervient. Construit sur OAuth 2.0, OIDC ajoute une couche d'identité, permettant aux applications clientes de vérifier l'identité de l'utilisateur final et d'obtenir des informations de profil basiques de manière standardisée et sécurisée.

Dans cette leçon, nous allons plonger au cœur d'OpenID Connect, comprendre ses mécanismes, ses composants clés et son fonctionnement pratique, afin de maîtriser son utilisation pour l'authentification de vos applications web.

Objectifs de la Leçon

À la fin de cette leçon, vous serez capable de :

  • Comprendre la relation entre OpenID Connect et OAuth 2.0.
  • Identifier les acteurs et les composants clés d'OpenID Connect.
  • Expliquer le fonctionnement du flux d'authentification OIDC principal (Authorization Code Flow).
  • Décrypter le rôle et la structure de l'ID Token (JWT).
  • Mettre en œuvre les bases d'une intégration OIDC simplifiée dans une application web.

1. Pourquoi OpenID Connect ? Le Problème de l'Identité avec OAuth 2.0

Avant OIDC, de nombreuses applications utilisaient des solutions basées sur OAuth 2.0 pour l'authentification, souvent en récupérant des informations utilisateur via des APIs de ressources (par exemple, /me endpoint). Cependant, cette approche présentait plusieurs inconvénients :

  • Non Standardisé : Chaque fournisseur d'identité (Google, Facebook, Twitter) avait ses propres API pour récupérer les informations de profil, forçant les clients à implémenter des logiques spécifiques pour chacun.
  • Identité Implicite : OAuth 2.0 fournit un access_token pour accéder à des ressources, mais ne garantit pas que cet access_token a été émis spécifiquement pour l'authentification d'un utilisateur. Il pourrait s'agir d'un jeton client-credentials, par exemple.
  • Manque de Garantie : Il n'y avait pas de moyen standardisé pour le client de vérifier que l'utilisateur qui avait initié la connexion était bien celui pour lequel l'accès avait été accordé.

OpenID Connect résout ces problèmes en fournissant une surcouche légère au protocole OAuth 2.0, spécifiquement conçue pour l'authentification. Il introduit un nouveau type de jeton, l'ID Token, qui contient des informations vérifiables sur l'identité de l'utilisateur et sur l'authentification elle-même.

2. Fondations : Rappel Bref d'OAuth 2.0

OpenID Connect est un protocole qui utilise les capacités d'autorisation d'OAuth 2.0. Il est donc essentiel de comprendre les rôles principaux d'OAuth 2.0.

Les Acteurs Clés d'OAuth 2.0

  • Resource Owner (Propriétaire de Ressource) : L'utilisateur final qui possède les données (ressources) à protéger. C'est lui qui donne son consentement.
  • Client (Application Cliente) : L'application qui souhaite accéder aux ressources du Resource Owner. C'est votre application web, mobile, etc.
  • Authorization Server (Serveur d'Autorisation) : Le serveur qui authentifie le Resource Owner, gère son consentement et émet les jetons (tokens).
  • Resource Server (Serveur de Ressources) : Le serveur qui héberge les ressources protégées de l'utilisateur (par exemple, les photos sur Google Photos, les emails sur Gmail). Il accepte et valide les jetons d'accès émis par le serveur d'autorisation.

Le Principe d'OAuth 2.0 : La Délégation

OAuth 2.0 permet au Resource Owner de déléguer l'accès à ses ressources à un Client sans lui donner son mot de passe. Au lieu de cela, le Client obtient un Access Token du Authorization Server, que le Client peut ensuite utiliser pour faire des requêtes au Resource Server au nom du Resource Owner.

Important : OAuth 2.0 est un protocole d'autorisation, pas d'authentification. Il répond à la question "Est-ce que cette application est autorisée à accéder à cette ressource ?" et non "Qui est l'utilisateur qui est en train de se connecter ?".

3. OpenID Connect : L'Identité en Plus

OpenID Connect étend OAuth 2.0 pour gérer l'identité. Il s'appuie sur les mêmes acteurs, mais leur donne des rôles légèrement modifiés pour la clarté :

Les Acteurs Clés d'OpenID Connect

  • End-User (Utilisateur Final) : L'équivalent du Resource Owner. C'est la personne qui se connecte.
  • Relying Party (RP) : L'équivalent du Client. C'est votre application qui dépend (rely) du fournisseur OpenID pour l'authentification.
  • OpenID Provider (OP) : L'équivalent de l'Authorization Server. C'est le serveur qui authentifie l'End-User et fournit l'ID Token et d'autres informations sur l'utilisateur. Il est aussi souvent le Resource Server pour les informations de profil utilisateur.

Les Composants Clés d'OIDC

OIDC introduit plusieurs concepts et spécifications additionnels qui le distinguent d'OAuth 2.0 :

3.1. L'ID Token : Le Cœur d'OIDC

L'ID Token est un JSON Web Token (JWT). C'est le composant le plus important d'OIDC. Il est émis par l'OpenID Provider et contient des "claims" (assertions) sur l'authentification de l'utilisateur et son identité.

Un JWT est une chaîne de caractères compacte, URL-safe, qui se compose de trois parties séparées par des points (.):

  1. Header (En-tête) : Contient des informations sur le type de jeton (JWT) et l'algorithme de signature utilisé (par exemple, HS256, RS256).
    {
      "alg": "RS256",
      "kid": "xyz123"
    }
    
  2. Payload (Charge utile) : Contient les claims. Ce sont des paires clé-valeur décrivant l'entité (ici, l'utilisateur) et des métadonnées. Il existe des claims enregistrés (standardisés), des claims publics et des claims privés.
    {
      "iss": "https://accounts.google.com",  // Issuer: l'OP qui a émis le token
      "azp": "1234567890.apps.googleusercontent.com", // Authorized party: le client ID
      "aud": "1234567890.apps.googleusercontent.com",  // Audience: le RP pour lequel le token est destiné
      "sub": "101234567890123456789",          // Subject: identifiant unique de l'utilisateur
      "hd": "example.com",                     // Hosted domain (optionnel)
      "email": "user@example.com",             // Email de l'utilisateur
      "email_verified": true,                  // Statut de vérification de l'email
      "at_hash": "abcdefg...",                 // Hash de l'Access Token
      "name": "Jane Doe",                      // Nom complet de l'utilisateur
      "picture": "https://example.com/user.jpg", // URL de la photo de profil
      "given_name": "Jane",                    // Prénom
      "family_name": "Doe",                    // Nom de famille
      "locale": "en",                          // Langue de l'utilisateur
      "iat": 1678886400,                       // Issued At: Timestamp d'émission du token
      "exp": 1678890000,                       // Expiration: Timestamp d'expiration du token
      "nonce": "a-random-string-123"           // Nonce (pour la protection contre les attaques par relecture)
    }
    
  3. Signature : Créée en encodant l'en-tête et la charge utile en Base64Url, puis en les signant avec la clé privée de l'OP. La Relying Party (RP) utilise la clé publique de l'OP pour vérifier cette signature, garantissant l'intégrité et l'authenticité du jeton.

Claims Importants de l'ID Token :

  • iss (Issuer) : L'identifiant de l'OpenID Provider (URL). Le RP doit vérifier qu'il s'attend à recevoir des jetons de cet OP.
  • aud (Audience) : L'identifiant du client (RP). Le RP doit s'assurer que le jeton lui est bien destiné.
  • sub (Subject) : L'identifiant unique de l'utilisateur final au sein de l'OP. Ce sub doit être constant pour un même utilisateur chez le même OP.
  • exp (Expiration Time) : Le temps d'expiration du jeton, après lequel il ne doit plus être accepté.
  • iat (Issued At) : Le temps d'émission du jeton.
  • auth_time : Le temps où l'authentification de l'utilisateur a eu lieu.
  • nonce : Une valeur unique incluse dans la requête initiale et renvoyée dans l'ID Token. Elle aide à prévenir les attaques par relecture (replay attacks).

3.2. UserInfo Endpoint

En plus de l'ID Token (qui contient un sous-ensemble de claims), OIDC spécifie un UserInfo Endpoint. C'est une ressource protégée sur l'OpenID Provider qui contient des claims supplémentaires sur l'utilisateur final. La Relying Party peut y accéder en utilisant l'access_token reçu lors de l'authentification. C'est utile pour récupérer des informations plus complètes ou moins sensibles, non nécessaires dans l'ID Token compact.

3.3. Discovery Endpoint

Pour simplifier l'intégration, OIDC définit un Discovery Endpoint (souvent à /.well-known/openid-configuration). Ce point de terminaison fournit dynamiquement à la Relying Party toutes les informations nécessaires pour interagir avec l'OpenID Provider : les URL des endpoints (authorization_endpoint, token_endpoint, userinfo_endpoint), les algorithmes de signature supportés, les scopes disponibles, etc. Cela rend l'intégration d'un RP avec un OP beaucoup plus simple et robuste.

4. Le Flux d'Authentification OIDC : L'Authorization Code Flow

OpenID Connect peut utiliser différents flux (flows) définis par OAuth 2.0 (Implicit, Authorization Code, Hybrid). Le plus recommandé et le plus sécurisé pour les applications web côté serveur est l'Authorization Code Flow.

Voici les étapes clés de ce flux :

  1. Préparation de la Requête d'Authentification :

    • L'utilisateur clique sur "Se connecter avec Google/Facebook/etc." dans votre application (Relying Party).
    • Votre application redirige le navigateur de l'utilisateur vers l'authorization_endpoint de l'OpenID Provider (OP).
    • Cette redirection inclut plusieurs paramètres dans l'URL :
      • client_id : L'identifiant de votre application, fourni par l'OP lors de l'enregistrement.
      • redirect_uri : L'URL où l'OP doit rediriger l'utilisateur après l'authentification (doit être pré-enregistrée).
      • response_type=code : Indique que la RP attend un code d'autorisation.
      • scope=openid profile email : openid est obligatoire pour OIDC. Les autres scopes (comme profile pour le nom, email pour l'adresse email) demandent l'accès à des claims spécifiques.
      • state : Une chaîne de caractères générée aléatoirement par la RP pour prévenir les attaques CSRF (Cross-Site Request Forgery). Elle doit être conservée par la RP et vérifiée au retour.
      • nonce : Une chaîne de caractères générée aléatoirement par la RP pour l'ID Token afin de prévenir les attaques par relecture.
  2. Authentification et Consentement de l'Utilisateur :

    • L'utilisateur arrive sur la page de connexion de l'OP.
    • Il saisit ses identifiants et/ou donne son consentement pour que l'OP partage ses informations avec la Relying Party.
  3. Redirection avec Code d'Autorisation :

    • Après authentification et consentement, l'OP redirige le navigateur de l'utilisateur vers le redirect_uri spécifié par la RP.
    • L'URL de redirection inclut le code d'autorisation et le state qui avait été envoyé.
  4. Échange du Code contre les Jetons :

    • La Relying Party reçoit le code et le state. Elle vérifie que le state correspond à celui qu'elle a généré.
    • Côté serveur (sécurisé), la RP envoie une requête POST à l'token_endpoint de l'OP pour échanger le code contre les jetons.
    • Cette requête inclut : le client_id, le client_secret (une clé secrète pour authentifier le client, utilisée seulement côté serveur), le code, et le redirect_uri.
    • L'OP valide le code et les identifiants du client.
  5. Réception des Jetons :

    • Si la validation réussit, l'OP renvoie une réponse JSON contenant :
      • id_token : Le JWT signé, contenant les claims d'identité.
      • access_token : Le jeton d'accès OAuth 2.0, utilisé pour accéder à des ressources protégées (comme l'UserInfo Endpoint).
      • token_type : Le type de jeton d'accès (généralement Bearer).
      • expires_in : La durée de validité de l'Access Token en secondes.
      • refresh_token (optionnel) : Utilisé pour obtenir de nouveaux Access Tokens sans réauthentifier l'utilisateur.
  6. Validation de l'ID Token :

    • C'est l'étape la plus critique. La Relying Party doit valider l'ID Token pour s'assurer de son authenticité et de son intégrité.
    • Les validations incluent :
      • Signature : Vérifier la signature du JWT en utilisant la clé publique de l'OP (obtenue via le Discovery Endpoint ou JWKS URI).
      • iss (Issuer) : Le champ iss doit correspondre à l'URL de l'OP attendu.
      • aud (Audience) : Le champ aud doit contenir le client_id de la RP.
      • exp (Expiration) : Le jeton ne doit pas être expiré.
      • nonce : Si un nonce a été envoyé dans la requête initiale, il doit être présent et correspondre dans l'ID Token.
      • at_hash (si présent) : Vérifier que le hachage de l'Access Token correspond au at_hash dans l'ID Token (protège contre le mélange de jetons).
  7. Accès aux Informations de l'Utilisateur :

    • Une fois l'ID Token validé, la RP peut extraire les claims pour identifier l'utilisateur.
    • Si des informations supplémentaires sont nécessaires, la RP peut utiliser l'access_token pour appeler l'UserInfo Endpoint de l'OP.
  8. Session de l'Application :

    • La RP établit une session locale pour l'utilisateur (par exemple, en créant un cookie de session), marquant l'utilisateur comme authentifié.

Ce flux assure une séparation claire des responsabilités et maximise la sécurité en gardant le client_secret et l'échange de jetons côté serveur.

5. Mise en Pratique : Un Exemple Simplifié d'Intégration OIDC

Nous allons simuler les étapes clés de l'Authorization Code Flow avec des exemples de code pour une application web. Pour cet exemple, nous n'allons pas construire un OP complet, mais montrer comment votre Relying Party interagit avec un OP (comme Google, Okta, Auth0, etc.).

Scénario

Une application web simple (RP) souhaite authentifier un utilisateur via un OpenID Provider.

Étape 1 : Le Front-end – Lancement de l'Authentification

Côté client (navigateur), un bouton de connexion initie le processus en redirigeant l'utilisateur vers l'OP.

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Application OIDC</title>
    <style>
        body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; background-color: #f4f4f4; }
        .container { background-color: white; padding: 40px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); text-align: center; }
        button { background-color: #007bff; color: white; padding: 12px 25px; border: none; border-radius: 5px; cursor: pointer; font-size: 1.1em; transition: background-color 0.3s ease; }
        button:hover { background-color: #0056b3; }
        p { margin-top: 20px; color: #555; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Bienvenue sur notre Application</h1>
        <p>Veuillez vous connecter pour accéder à votre contenu personnalisé.</p>
        <button id="loginButton">Se connecter avec OpenID Connect</button>
    </div>

    <script>
        document.getElementById('loginButton').addEventListener('click', function() {
            // Ces valeurs seraient configurées en production
            const clientId = 'VOTRE_CLIENT_ID'; // L'ID que vous avez obtenu de l'OP
            const redirectUri = 'http://localhost:8080/callback'; // L'URL de redirection de votre application
            const authorizationEndpoint = 'https://accounts.google.com/o/oauth2/v2/auth'; // Exemple: Google
            
            // Générer un 'state' unique pour prévenir les attaques CSRF
            const state = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
            localStorage.setItem('oauth_state', state); // Stocker le state pour vérification ultérieure

            // Générer un 'nonce' unique pour l'ID Token
            const nonce = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
            localStorage.setItem('oauth_nonce', nonce); // Stocker le nonce

            const params = new URLSearchParams({
                client_id: clientId,
                redirect_uri: redirectUri,
                response_type: 'code', // Nous voulons un code d'autorisation
                scope: 'openid profile email', // openid est obligatoire; profile et email pour des infos de base
                state: state,
                nonce: nonce,
                prompt: 'select_account' // Optionnel: force la sélection du compte Google
            });

            window.location.href = `${authorizationEndpoint}?${params.toString()}`;
        });
    </script>
</body>
</html>

Explication du code HTML/JavaScript :

  • Le script s'attache à un bouton de connexion.
  • clientId, redirectUri, et authorizationEndpoint sont des placeholders. En production, clientId serait votre identifiant unique fourni par l'OpenID Provider (ex: Google Developer Console), redirectUri l'URL de votre backend qui gérera le retour de l'OP, et authorizationEndpoint l'URL spécifique à l'OP.
  • state et nonce sont des chaînes aléatoires générées et stockées localement. Elles sont cruciales pour la sécurité :
    • state est renvoyé par l'OP et est vérifié côté serveur pour s'assurer que la réponse provient de la requête initiale de notre utilisateur.
    • nonce est également renvoyé dans l'ID Token et sert à lier l'ID Token à la requête initiale, empêchant les attaques par relecture.
  • Les paramètres sont encodés et ajoutés à l'URL de l'authorizationEndpoint. response_type=code indique que nous utilisons l'Authorization Code Flow. scope=openid profile email demande l'accès aux claims d'identité, de profil et d'email.
  • window.location.href = ... redirige le navigateur de l'utilisateur vers l'OpenID Provider.

Étape 2 : Le Back-end – Traitement de la Redirection et Échange des Jetons

Une fois que l'utilisateur s'est authentifié auprès de l'OP, l'OP le redirige vers votre redirect_uri (par exemple, http://localhost:8080/callback). C'est le rôle de votre serveur de gérer cette requête.

Pour cet exemple, nous allons utiliser un serveur Node.js avec Express, mais le principe est le même pour PHP, Python, Java, etc.

// server.js (Node.js avec Express)
const express = require('express');
const axios = require('axios'); // Pour faire des requêtes HTTP
const jwt = require('jsonwebtoken'); // Pour décoder et vérifier les JWT
const jwksClient = require('jwks-rsa'); // Pour récupérer les clés publiques de l'OP

const app = express();
const port = 8080;

// Configuration de votre application
const CLIENT_ID = 'VOTRE_CLIENT_ID'; // ID de votre application chez l'OP
const CLIENT_SECRET = 'VOTRE_CLIENT_SECRET'; // Secret de votre application chez l'OP
const REDIRECT_URI = 'http://localhost:8080/callback';

// Information de l'OpenID Provider (Google dans cet exemple)
const ISSUER_URL = 'https://accounts.google.com';
const DISCOVERY_URL = `${ISSUER_URL}/.well-known/openid-configuration`;

let opConfig = null; // Configuration de l'OP découverte

// Middleware pour récupérer la configuration de l'OP au démarrage
app.use(async (req, res, next) => {
    if (!opConfig) {
        try {
            const response = await axios.get(DISCOVERY_URL);
            opConfig = response.data;
            console.log('Configuration de l\'OP découverte:', opConfig);
        } catch (error) {
            console.error('Erreur lors de la découverte de l\'OP:', error.message);
            return res.status(500).send('Erreur de configuration du serveur.');
        }
    }
    next();
});

// Endpoint de la page d'accueil (pour lancer le login)
app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html'); // suppose que index.html est dans le même répertoire
});

// Endpoint de callback après la redirection de l'OP
app.get('/callback', async (req, res) => {
    const { code, state, error, error_description } = req.query;

    if (error) {
        console.error('Erreur de l\'OP:', error, error_description);
        return res.status(400).send(`Erreur de connexion: ${error_description || error}`);
    }

    // 1. Vérifier le paramètre 'state'
    const storedState = localStorage.getItem('oauth_state'); // Attention: localStorage n'est pas accessible côté serveur directement.
                                                              // En production, le 'state' serait stocké dans la session du serveur.
    // Pour cet exemple simpliste, nous allons ignorer la vérification de 'state' côté serveur
    // si vous le stockez côté client. Idéalement, utilisez express-session ou équivalent.
    // if (!state || state !== storedState) {
    //     console.error('Erreur de State CSRF:', state, storedState);
    //     return res.status(403).send('Erreur de sécurité: State invalide.');
    // }
    // localStorage.removeItem('oauth_state'); // Nettoyer après vérification

    // 2. Échanger le code contre les tokens
    try {
        const tokenResponse = await axios.post(opConfig.token_endpoint, {
            client_id: CLIENT_ID,
            client_secret: CLIENT_SECRET,
            code: code,
            redirect_uri: REDIRECT_URI,
            grant_type: 'authorization_code'
        }, {
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        });

        const { id_token, access_token, expires_in } = tokenResponse.data;

        console.log('Tokens reçus:');
        console.log('ID Token:', id_token);
        console.log('Access Token:', access_token);

        // 3. Valider l'ID Token
        const decodedIdToken = jwt.decode(id_token, { complete: true });
        console.log('ID Token décodé (header & payload):', decodedIdToken);

        const client = jwksClient({
            jwksUri: opConfig.jwks_uri // Récupéré via la découverte de l'OP
        });

        function getKey(header, callback) {
            client.getSigningKey(header.kid, function (err, key) {
                const signingKey = key.publicKey || key.rsaPublicKey;
                callback(null, signingKey);
            });
        }

        jwt.verify(id_token, getKey, {
            audience: CLIENT_ID,
            issuer: ISSUER_URL,
            algorithms: ['RS256'] // ou l'algorithme spécifié dans le header de l'ID Token
            // nonce: localStorage.getItem('oauth_nonce') // Idem pour nonce, stocker en session
        }, async (err, verifiedClaims) => {
            if (err) {
                console.error('Erreur de validation ID Token:', err.message);
                return res.status(401).send('ID Token invalide.');
            }

            console.log('ID Token validé. Claims:', verifiedClaims);

            // localStorage.removeItem('oauth_nonce'); // Nettoyer

            // 4. (Optionnel) Utiliser l'Access Token pour récupérer plus d'informations via UserInfo Endpoint
            if (opConfig.userinfo_endpoint && access_token) {
                try {
                    const userinfoResponse = await axios.get(opConfig.userinfo_endpoint, {
                        headers: {
                            'Authorization': `Bearer ${access_token}`
                        }
                    });
                    console.log('Informations utilisateur (UserInfo Endpoint):', userinfoResponse.data);
                    // Stocker userinfoResponse.data et/ou verifiedClaims dans la session utilisateur
                    res.send(`
                        <h1>Authentification réussie !</h1>
                        <p>Bienvenue, ${verifiedClaims.name || verifiedClaims.email || verifiedClaims.sub}!</p>
                        <pre>ID Token Claims: ${JSON.stringify(verifiedClaims, null, 2)}</pre>
                        <pre>User Info Claims: ${JSON.stringify(userinfoResponse.data, null, 2)}</pre>
                        <p>Ceci est une démonstration. En production, vous établiriez une session ici.</p>
                    `);
                } catch (userinfoError) {
                    console.error('Erreur lors de l\'appel à UserInfo Endpoint:', userinfoError.message);
                    res.status(500).send('Erreur lors de la récupération des informations utilisateur.');
                }
            } else {
                res.send(`
                    <h1>Authentification réussie !</h1>
                    <p>Bienvenue, ${verifiedClaims.name || verifiedClaims.email || verifiedClaims.sub}!</p>
                    <pre>ID Token Claims: ${JSON.stringify(verifiedClaims, null, 2)}</pre>
                    <p>Ceci est une démonstration. En production, vous établiriez une session ici.</p>
                `);
            }
        });

    } catch (error) {
        console.error('Erreur lors de l\'échange de code ou de la validation:', error.response ? error.response.data : error.message);
        res.status(500).send('Erreur serveur lors de l\'authentification.');
    }
});

app.listen(port, () => {
    console.log(`Serveur démarré sur http://localhost:${port}`);
    console.log('N\'oubliez pas de remplacer VOTRE_CLIENT_ID et VOTRE_CLIENT_SECRET !');
});

Pour exécuter ce code Node.js, vous aurez besoin d'installer les dépendances : npm install express axios jsonwebtoken jwks-rsa

Explication du code Node.js :

  • Initialisation : Configure les variables essentielles (CLIENT_ID, CLIENT_SECRET, REDIRECT_URI) et l'URL de l'OpenID Provider.
  • Découverte de l'OP (opConfig) : Avant de traiter les callbacks, le serveur récupère la configuration dynamique de l'OP via son Discovery Endpoint. Cela permet d'obtenir les URLs des points de terminaison et des clés publiques (jwks_uri) nécessaires.
  • GET /callback : Cet endpoint est appelé par l'OP après l'authentification de l'utilisateur.
    • Il récupère le code et le state des paramètres de requête.
    • Vérification du state : Une étape cruciale mais simplifiée ici. En production, le state doit être stocké dans la session du serveur avant la redirection et comparé à celui reçu, pour prévenir les attaques CSRF.
    • Échange du code (axios.post) : Le serveur envoie une requête POST sécurisée (utilisant CLIENT_ID et CLIENT_SECRET) à l'token_endpoint de l'OP pour échanger le code contre l'id_token et l'access_token.
    • Validation de l'ID Token :
      • L'id_token est un JWT signé. Il est d'abord décodé pour inspecter son en-tête et sa charge utile.
      • La bibliothèque jwks-rsa est utilisée pour récupérer dynamiquement la clé publique de l'OP (via son jwks_uri). Cette clé est ensuite utilisée par jsonwebtoken.verify pour vérifier la signature du JWT.
      • jwt.verify effectue également des vérifications essentielles sur les claims aud (audience), iss (issuer), et exp (expiration). Le nonce devrait également être vérifié ici s'il était stocké côté serveur.
    • Appel à UserInfo Endpoint (optionnel) : Si l'OP fournit un userinfo_endpoint et que la RP a besoin de plus de claims que ceux présents dans l'ID Token, l'access_token peut être utilisé pour faire une requête GET à cet endpoint.
    • Établissement de la session : Une fois l'ID Token validé et les informations utilisateur obtenues, la Relying Party peut établir sa propre session locale pour l'utilisateur (par exemple, en créant un cookie de session côté serveur), marquant ainsi l'utilisateur comme authentifié.

Cet exemple est simplifié et omet certaines considérations de production (gestion d'erreurs plus robustes, gestion de session serveur pour state/nonce, rafraîchissement des tokens, etc.), mais il illustre les étapes fondamentales de l'intégration OIDC.

6. Avantages d'OpenID Connect

L'adoption d'OpenID Connect apporte de nombreux bénéfices :

  • Standardisation : Simplifie grandement l'intégration avec différents fournisseurs d'identité, réduisant la complexité de développement et de maintenance.
  • Simplicité : Pour les Relying Parties, OIDC est plus simple à implémenter pour l'authentification que des solutions ad-hoc basées uniquement sur OAuth 2.0.
  • Sécurité Améliorée :
    • L'ID Token signé et les validations strictes (signature, iss, aud, exp, nonce, at_hash) garantissent l'authenticité et l'intégrité des informations d'identité.
    • L'utilisation du state prévient les attaques CSRF.
    • L'Authorization Code Flow évite de passer des tokens sensibles via l'URL du navigateur.
  • Interoperabilité : L'écosystème OIDC est vaste, avec de nombreux OP (Google, Microsoft, Auth0, Okta, Keycloak) et bibliothèques clientes disponibles.
  • Flexibilité : Permet de demander des informations de profil spécifiques via les scopes et le UserInfo Endpoint.

7. Limites et Considérations

Bien qu'OIDC soit un outil puissant, il est important de noter ses limites :

  • Authentification, Pas Autorisation Fine : OIDC gère l'authentification ("Qui êtes-vous ?"). Il ne gère pas nativement les rôles ou les permissions ("Qu'avez-vous le droit de faire ?"). Pour l'autorisation, il est souvent combiné avec un système de contrôle d'accès basé sur les rôles (RBAC) ou les attributs (ABAC) dans votre application. L'Access Token obtenu via OAuth 2.0 est ce qui est utilisé pour l'autorisation vis-à-vis des Resource Servers.
  • Complexité Initiale : Bien que plus simple que des solutions maison, OIDC reste un protocole avec plusieurs étapes et concepts. L'implémentation sans bibliothèque peut être complexe. Il est fortement recommandé d'utiliser des bibliothèques OIDC ou des SDKs clients.
  • Dépendance à l'OP : Votre application dépend de la disponibilité et de la performance de l'OpenID Provider pour l'authentification.
  • Gestion des Sessions : OIDC authentifie l'utilisateur. La Relying Party doit ensuite gérer sa propre session utilisateur (cookies, jetons de session) pour éviter de renvoyer l'utilisateur vers l'OP à chaque requête.

Conclusion

OpenID Connect est devenu le standard de facto pour l'authentification des utilisateurs dans les applications web modernes. En tirant parti des fondations d'OAuth 2.0 et en y ajoutant une couche d'identité robuste et standardisée via l'ID Token, il résout le défi de la vérification d'identité de manière élégante et sécurisée.

Nous avons exploré comment OIDC s'intègre au-dessus d'OAuth 2.0, les rôles des différents acteurs, les composants clés comme l'ID Token et les endpoints de découverte, et le fonctionnement détaillé de l'Authorization Code Flow. L'exemple pratique a montré comment une Relying Party initie l'authentification et traite la réponse du fournisseur d'identité.

Maîtriser OpenID Connect est une compétence essentielle pour tout développeur construisant des applications web sécurisées et interopérables. Il permet non seulement une expérience utilisateur fluide avec le "Single Sign-On" mais aussi une architecture d'authentification fiable et maintenable.

Dans les prochaines leçons de ce cours, nous approfondirons les sujets liés à l'autorisation, la gestion des sessions, la sécurité des API et d'autres aspects avancés de l'authentification pour compléter votre expertise dans ce domaine crucial.