Implémentation de la technologie SSO basée sur Node.js

Les applications Web sont créées à l'aide d'une architecture client-serveur utilisant HTTP comme protocole de communication. HTTP est un protocole sans état. Chaque fois qu'un navigateur envoie une requête au serveur, le serveur traite cette requête indépendamment des autres requêtes et ne l'associe pas aux requêtes précédentes ou suivantes du même navigateur. Cela signifie, entre autres, que n'importe qui peut accéder aux ressources du serveur qui ne sont en aucun cas protégées. Si vous devez protéger certaines ressources du serveur contre des tiers, cela signifie que vous devez en quelque sorte limiter ce que le navigateur peut demander au serveur. Autrement dit, vous devez authentifier les demandes et répondre uniquement à celles qui ont réussi la vérification, en ignorant celles qui n'ont pas réussi la vérification. Pour authentifier les demandes, vous devez avoir des informations sur les demandes,stocké côté navigateur. Étant donné que le protocole HTTP ne stocke pas l'état des requêtes, nous avons besoin de mécanismes supplémentaires pour cela qui permettent au serveur et au navigateur de gérer conjointement l'état des connexions. Ces mécanismes incluent l'utilisation de cookies, de sessions, de JWT.







Si nous parlons d'un seul projet Web, les informations sur l'état d'une session particulière d'interaction entre le client et le serveur sont faciles à maintenir en utilisant l'authentification de l'utilisateur lors de sa connexion. Mais si un tel système indépendant évolue, se transformant en plusieurs systèmes, le développeur est confronté à la question de la conservation des informations sur l'état de chacun de ces systèmes séparés. En pratique, cette question ressemble à ceci: "L'utilisateur de ces systèmes devra-t-il entrer chacun d'eux séparément et également en sortir?"



Il existe une bonne règle de base concernant les systèmes dont la complexité augmente avec le temps et la manière dont ces systèmes interagissent avec leurs utilisateurs. À savoir, le fardeau de résoudre les problèmes associés à la complication de l'architecture du projet incombe au système et non à ses utilisateurs. Peu importe la complexité des mécanismes internes du projet Web. Cela devrait ressembler à un système unifié pour l'utilisateur. En d'autres termes, un utilisateur travaillant avec un système Web composé de nombreux composants doit percevoir ce qui se passe comme s'il travaillait avec un seul système. En particulier, nous parlons d'authentification dans de tels systèmes utilisant SSO (Single Sign-On) - une technologie d'authentification unique.



Comment créer des systèmes qui utilisent SSO? Vous pourriez penser à la bonne vieille solution basée sur les cookies ici, mais cette solution est soumise à des limitations. Les restrictions s'appliquent aux domaines à partir desquels les cookies sont installés. Il ne peut être contourné qu'en collectant tous les noms de domaine de tous les sous-systèmes de l'application Web sur un domaine de premier niveau.



Dans l'environnement actuel, ces solutions sont entravées par l'adoption généralisée des architectures de microservices. La gestion de session est devenue plus compliquée à un moment où différentes technologies étaient utilisées dans le développement de projets Web, et où différents services étaient parfois hébergés sur des domaines différents. De plus, les services Web qui étaient écrits en Java ont commencé à écrire en utilisant les capacités de la plate-forme Node.js. Cela a rendu plus difficile le travail avec les cookies. Il s'est avéré que les sessions ne sont plus si faciles à gérer.



Ces difficultés ont conduit au développement de nouvelles méthodes de connexion aux systèmes, en particulier, nous parlons de la technologie de l'authentification unique.



Technologie d'authentification unique



Le principe de base sur lequel repose la technologie d'authentification unique est qu'un utilisateur peut se connecter à un système d'un projet composé de plusieurs systèmes et être autorisé dans tous les autres systèmes sans avoir à se reconnecter. Dans le même temps, nous parlons d'une sortie centralisée de tous les systèmes.



Nous allons, à des fins éducatives, implémenter la technologie SSO sur la plate-forme Node.js.



Il est à noter que la mise en œuvre de cette technologie à l'échelle de l'entreprise demandera beaucoup plus d'efforts que ce que nous allons consacrer au développement de notre système de formation. C'est pourquoi il existe des solutions SSO spécialisées conçues pour les projets de grande envergure.



Comment la connexion SSO est-elle organisée?



Au cœur de la mise en œuvre SSO se trouve un serveur d'authentification unique et indépendant capable d'accepter des informations pour authentifier les utilisateurs. Par exemple - adresse e-mail, nom d'utilisateur, mot de passe. D'autres systèmes ne fournissent pas à l'utilisateur de mécanismes directs pour s'y connecter. Ils autorisent indirectement l'utilisateur en recevant des informations le concernant du serveur d'authentification. Les mécanismes d'autorisation indirecte sont mis en œuvre à l'aide de jetons.



Voici le référentiel de code du projet simple-sso, dont je décrirai l'implémentation ici. J'utilise le framework Node.js, mais vous pouvez l'implémenter en utilisant quelque chose de différent. Prenons une analyse étape par étape des actions de l'utilisateur travaillant avec le système et des mécanismes qui composent ce système.



Étape 1



L'utilisateur tente d'accéder à une ressource protégée sur le système (appelons cette ressource le "consommateur SSO", "sso-consumer"). Le consommateur SSO découvre que l'utilisateur n'est pas connecté et redirige l'utilisateur vers le «serveur SSO» («serveur sso») en utilisant sa propre adresse comme paramètre de requête. Un utilisateur authentifié avec succès sera redirigé vers cette adresse. Ce mécanisme est fourni par le middleware Express:



const isAuthenticated = (req, res, next) => {
  //   ,   ,
  //     -     SSO-     
  //    URL  URL,     
  // ,   
  const redirectURL = `${req.protocol}://${req.headers.host}${req.path}`;
  if (req.session.user == null) {
    return res.redirect(
      `http://sso.ankuranand.com:3010/simplesso/login?serviceURL=${redirectURL}`
    );
  }
  next();
};

module.exports = isAuthenticated;


Étape 2



Le serveur SSO découvre que l'utilisateur n'est pas connecté et le redirige vers la page de connexion:



const login = (req, res, next) => {
  //  req.query  url,      
  //    ,     sso-.
  //        
  //     
  const { serviceURL } = req.query;
  //         URL.
  if (serviceURL != null) {
    const url = new URL(serviceURL);
    if (alloweOrigin[url.origin] !== true) {
      return res
        .status(400)
        .json({ message: "Your are not allowed to access the sso-server" });
    }
  }
  if (req.session.user != null && serviceURL == null) {
    return res.redirect("/");
  }
  //          -  
  //   
  if (req.session.user != null && serviceURL != null) {
    const url = new URL(serviceURL);
    const intrmid = encodedId();
    storeApplicationInCache(url.origin, req.session.user, intrmid);
    return res.redirect(`${serviceURL}?ssoToken=${intrmid}`);
  }

  return res.render("login", {
    title: "SSO-Server | Login"
  });
};


Je vais faire quelques commentaires ici concernant la sécurité.



Nous vérifions le serviceURLparamètre de demande entrante sur le serveur SSO. Cela nous permet de savoir si cette URL est enregistrée dans le système et si le service qu'elle représente peut utiliser les services d'un serveur SSO.



Voici à quoi pourrait ressembler une liste d'URL pour les services autorisés à utiliser le serveur SSO:



const alloweOrigin = {
"http://consumer.ankuranand.in:3020": true,
"http://consumertwo.ankuranand.in:3030": true,
"http://test.tangledvibes.com:3080": true,
"http://blog.tangledvibes.com:3080": fasle,
};


Étape 3



L'utilisateur entre un nom d'utilisateur et un mot de passe qui sont envoyés au serveur SSO dans la demande de connexion.





Page de connexion



Étape 4



Le serveur d'authentification SSO vérifie les informations de l'utilisateur et crée une session entre lui-même et l'utilisateur. C'est la soi-disant "session globale". Un jeton d'autorisation est créé immédiatement. Le jeton est une chaîne de caractères aléatoires. La manière exacte dont cette chaîne est générée n'a pas d'importance. L'essentiel est que des lignes similaires ne soient pas répétées pour différents utilisateurs et qu'une telle ligne serait difficile à forger.



Étape 5



Le serveur SSO prend le jeton d'autorisation et le transmet à l'endroit d'où vient l'utilisateur nouvellement connecté (c'est-à-dire qu'il transmet le jeton au consommateur SSO).



const doLogin = (req, res, next) => {
  //         .
  //         , 
  // userDB -   ,   ,   
  const { email, password } = req.body;
  if (!(userDB[email] && password === userDB[email].password)) {
    return res.status(404).json({ message: "Invalid email and password" });
  }

  //     
  const { serviceURL } = req.query;
  const id = encodedId();
  req.session.user = id;
  sessionUser[id] = email;
  if (serviceURL == null) {
    return res.redirect("/");
  }
  const url = new URL(serviceURL);
  const intrmid = encodedId();
  storeApplicationInCache(url.origin, id, intrmid);
  return res.redirect(`${serviceURL}?ssoToken=${intrmid}`);
};


Encore une fois, quelques notes de sécurité:



  • Ce token doit toujours être considéré comme un mécanisme intermédiaire, il est utilisé pour obtenir un autre token.
  • Si vous utilisez le JWT comme jeton intermédiaire, essayez de ne pas y inclure de secrets.


Étape 6



Le consommateur SSO reçoit un jeton et contacte le serveur SSO pour vérifier le jeton. Le serveur vérifie le jeton et renvoie un autre jeton avec des informations utilisateur. Ce jeton est utilisé par le consommateur SSO pour créer une session avec l'utilisateur. Cette session est appelée locale.



Voici le code middleware utilisé dans le consommateur SSO basé sur Express:



const ssoRedirect = () => {
  return async function(req, res, next) {
    // ,    req queryParameter,  ssoToken,
    //  ,    .
    const { ssoToken } = req.query;
    if (ssoToken != null) {
      //   ssoToken   ,  .
      const redirectURL = url.parse(req.url).pathname;
      try {
        const response = await axios.get(
          `${ssoServerJWTURL}?ssoToken=${ssoToken}`,
          {
            headers: {
              Authorization: "Bearer l1Q7zkOL59cRqWBkQ12ZiGVW2DBL"
            }
          }
        );
        const { token } = response.data;
        const decoded = await verifyJwtToken(token);
        //      jwt,  
        // global-session-id  id ,  
        //         .
        req.session.user = decoded;
      } catch (err) {
        return next(err);
      }

      return res.redirect(`${redirectURL}`);
    }

    return next();
  };
};


Après avoir reçu une demande d'un consommateur SSO, le serveur vérifie le jeton pour son existence et sa date d'expiration. Le jeton vérifié est considéré comme valide.



Dans notre cas, le serveur SSO, après vérification réussie du jeton, retourne un JWT signé avec des informations sur l'utilisateur.



const verifySsoToken = async (req, res, next) => {
  const appToken = appTokenFromRequest(req);
  const { ssoToken } = req.query;
  //        ssoToken .
  //  ssoToken    - ,   .
  if (
    appToken == null ||
    ssoToken == null ||
    intrmTokenCache[ssoToken] == null
  ) {
    return res.status(400).json({ message: "badRequest" });
  }

  //  appToken  -     
  const appName = intrmTokenCache[ssoToken][1];
  const globalSessionToken = intrmTokenCache[ssoToken][0];
  //  appToken   ,   SSO-        
  if (
    appToken !== appTokenDB[appName] ||
    sessionApp[globalSessionToken][appName] !== true
  ) {
    return res.status(403).json({ message: "Unauthorized" });
  }
  // ,     
  const payload = generatePayload(ssoToken);

  const token = await genJwtToken(payload);
  //    ,     
  delete intrmTokenCache[ssoToken];
  return res.status(200).json({ token });
};


Voici quelques notes de sécurité.



  • Toutes les applications qui utiliseront ce serveur pour l'authentification doivent être enregistrées auprès du serveur SSO. Ils doivent se voir attribuer des codes qui seront utilisés pour les vérifier lorsqu'ils font des demandes au serveur. Cela permet un niveau de sécurité plus élevé lors de la communication entre le serveur SSO et les consommateurs SSO.
  • Il est possible de générer différents fichiers rsa «privés» et «publics» pour chaque application et de laisser chacun d'eux vérifier leurs JWT en interne avec leurs clés publiques respectives.


De plus, vous pouvez définir une politique de sécurité au niveau de l'application et organiser son stockage centralisé:



const userDB = {
  "info@ankuranand.com": {
    password: "test",
    userId: encodedId(), //   ,         .
    appPolicy: {
      sso_consumer: { role: "admin", shareEmail: true },
      simple_sso_consumer: { role: "user", shareEmail: false }
    }
  }
};


Une fois que l'utilisateur s'est connecté avec succès au système, des sessions sont créées entre lui et le serveur SSO, ainsi qu'entre lui et chaque sous-système. La session établie entre l'utilisateur et le serveur SSO est appelée session globale. Une session établie entre un utilisateur et un sous-système qui fournit à l'utilisateur certains services est appelée session locale. Une fois la session locale établie, l'utilisateur pourra travailler avec les ressources du sous-système fermées aux ressources étrangères.





Mise en place de sessions locales et globales



Présentation rapide du consommateur SSO et du serveur SSO



Prenons un rapide tour d'horizon des fonctionnalités du consommateur SSO et du serveur SSO.



▍ Consommateur SSO



  1. Le sous-système consommateur SSO n'authentifie pas l'utilisateur en redirigeant l'utilisateur vers le serveur SSO.
  2. Ce sous-système reçoit le jeton qui lui est transmis par le serveur SSO.
  3. Il interagit avec le serveur, vérifiant la validité du jeton.
  4. Elle reçoit le JWT et valide ce jeton à l'aide de la clé publique.
  5. Ce sous-système établit une session locale.


Serveur SSO



  1. Le serveur SSO valide les informations de connexion de l'utilisateur.
  2. Le serveur crée une session globale.
  3. Il crée un jeton d'autorisation.
  4. Un jeton d'autorisation est envoyé au consommateur SSO.
  5. Le serveur vérifie la validité des jetons qui lui sont transmis par les consommateurs SSO.
  6. Le serveur envoie le SSO JWT au consommateur avec les informations utilisateur.


Organisation de la déconnexion centralisée



De la même manière que la mise en œuvre de SSO, vous pouvez implémenter la technologie SSO. Ici, il vous suffit de prendre en compte les considérations suivantes:



  1. Si une session locale existe, une session globale doit également exister.
  2. Si une session globale existe, cela ne signifie pas nécessairement qu'une session locale existe.
  3. Si la session locale est détruite, la session globale doit également être détruite.


Résultat



Par conséquent, il peut être noté qu'il existe de nombreuses implémentations prêtes à l'emploi de la technologie d'authentification unique que vous pouvez intégrer dans votre système. Ils ont tous leurs propres avantages et inconvénients. Développer un tel système indépendamment, à partir de zéro, est un processus itératif au cours duquel vous devez analyser les caractéristiques de chacun des systèmes. Cela inclut les méthodes de connexion, le stockage des informations utilisateur, la synchronisation des données, etc.



Vos projets utilisent-ils des mécanismes SSO?






All Articles