Source: component/serviceServer.js

/**
 * This file is part of Sesatheque.
 *   Copyright 2014-2015, Association Sésamath
 *
 * Sesatheque is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation.
 *
 * Sesatheque is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Sesatheque (LICENCE.txt).
 * @see http://www.gnu.org/licenses/agpl.txt
 *
 *
 * Ce fichier fait partie de l'application Sésathèque, créée par l'association Sésamath.
 *
 * Sésathèque est un logiciel libre ; vous pouvez le redistribuer ou le modifier suivant
 * les termes de la GNU Affero General Public License version 3 telle que publiée par la
 * Free Software Foundation.
 * Sésathèque est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE ;
 * sans même la garantie tacite de QUALITÉ MARCHANDE ou d'ADÉQUATION à UN BUT PARTICULIER.
 * Consultez la GNU Affero General Public License pour plus de détails.
 * Vous devez avoir reçu une copie de la GNU General Public License en même temps que Sésathèque
 * (cf LICENCE.txt et http://vvlibri.org/fr/Analyse/gnu-affero-general-public-license-v3-analyse
 * pour une explication en français)
 */
'use strict'
const { getToken, hasProp } = require('sesajstools')
const log = require('sesajstools/utils/log')
// const sjtUrl = require('sesajstools/http/url')
const User = require('./User')
const { getUserKey } = require('./common')
const { getBaseUrl, getClients, getProp, getTtl, init } = require('./helperServer')
const { errorCallback, setErrorCallback } = require('./helper')

const instanceId = process.env.NODE_APP_INSTANCE || 0
const ssoLog = (str) => log(`sesalabSso[${instanceId}], ${str}`)

module.exports = function (component) {
  component.service('$sesalabSsoServer', function ($cache, $settings, $sesalabSso) {
    const myBaseId = $settings.get('application.baseId')

    /**
     * Retourne la liste des index (dans la liste de clients courants) correspondants aux baseUrl fournies
     * @private
     * @param {string[]} clientsBases liste de baseUrl à rechercher dans les clients courants
     * @returns {number[]}
     */
    function getClientsIndexes (clientsBases) {
      const clientsIndexes = []
      const allClients = getClients()
      if (clientsBases) {
        // on cherche l'index parmi nos clients
        let seekIndex
        clientsBases.forEach(function (baseUrl) {
          seekIndex = null
          allClients.some(function (client, index) {
            if (client.baseUrl === baseUrl) {
              seekIndex = index
              return true
            }
            return false
          })
          if (seekIndex !== null) clientsIndexes.push(seekIndex)
          else console.error(new Error('Aucun client n’a baseUrl=' + baseUrl))
        })
      } else {
        allClients.forEach((c, i) => {
          clientsIndexes.push(i)
        })
      }
      return clientsIndexes
    }

    /**
     * génère un token, met en cache le user associé et redirige vers le client
     * @param context
     * @param client
     * @param user
     */
    function redirectToClient (context, client, user) {
      // console.log('redirectToClient', client)
      const token = getToken()
      const cacheKey = getUserKey(token)
      const ttl = getTtl()
      const url = `${client.baseUrl}${client.login}?token=${token}&authBaseUrl=${encodeURIComponent(getBaseUrl())}&retour=${encodeURIComponent(getProp('afterlogin'))}`
      $cache.set(cacheKey, user, ttl, function (error) {
        ssoLog(`mise en cache ${error ? 'KO' : 'OK'} du user ${user.nom} avec la clé ${cacheKey} pour ${ttl}s, avant redirect vers ${url}`)
        if (error) return errorCallback(context, error)
        context.redirect(url)
      })
    }

    // ///////////////////
    // Méthodes publiques
    // ///////////////////

    /**
     * Retourne la liste des urls de logout des clients,
     * pour la fournir dans le navigateur à notre fct sesalabSso_logout de logout.bundle.js
     * @memberOf $sesalabSsoServer
     * @returns {string[]}
     */
    function getClientsLogoutUrl (context) {
      // on ne renvoie que celles des serveurs sur lesquels on est connecté
      const indexesOk = context.session && context.session.sesalabSso && context.session.sesalabSso.clientOk
      const allClients = getClients()
      if (indexesOk && indexesOk.length) {
        return indexesOk.map((index) => allClients[index].baseUrl + allClients[index].logout)
      }
      return []
    }

    /**
     * Lance le chaînage des redirections vers les clients
     * - si appUser est fourni, réinitialise la session sesalabSso avec la liste des logins à propager
     *   (vers les clientBases si fourni, ou par défaut vers tous les clients référencés)
     * - sinon, regarde s'il reste des propagations pour les faire, et redirige vers afterLogin sinon
     * @memberOf $sesalabSsoServer
     * @param {context} context
     * @param {object} [appUser] user de l'appli serveur qui sera casté en User
     * @param {string[]} [clientBases] Une éventuelle liste de baseUrl de clients si on veut pas tous les appeler
     */
    function loginOnClients (context, appUser, clientBases) {
      try {
        if (!myBaseId) throw new Error('baseId non renseignée pour cette application')
        const clients = getClients()
        if (!context.session.sesalabSso) context.session.sesalabSso = {}
        // raccourci d'écriture
        const s = context.session.sesalabSso

        // init si on fournit un client
        if (appUser && appUser.oid) {
          ssoLog(`loginOnClients init avec ${appUser.nom} (${appUser.login})`)
          // on repart à 0
          $sesalabSso.flushAuthTokens(context)
          // le user que l'on va transmettre aux clients
          if (!appUser.pid) appUser.pid = myBaseId + '/' + appUser.oid
          const user = new User(appUser)
          // faut le mettre en session pour le retrouver aux redirects suivants
          s.user = user
          s.clientsIndexes = getClientsIndexes(clientBases)
          s.clientIndex = 0
          s.clientOk = []
          s.clientKo = []
          redirectToClient(context, clients[0], user)
          return
        }

        // sinon, redirect vers le client suivant
        if (hasProp(s, 'clientIndex')) {
          // mémorisation du dernier redirect
          if (context.get.success === 'true') s.clientOk.push(s.clientIndex)
          else s.clientKo.push(s.clientIndex)
          // incrément du client
          s.clientIndex++

          // retour vers l'url de fin sur le serveur si c'est fini
          if (s.clientIndex >= s.clientsIndexes.length) {
            ssoLog(`loginOnClients last avec ${s.user && s.user.nom}, redirect vers afterClientsPage`)
            // si c'est le dernier on arrête là, on nettoie un peu la session
            delete s.user
            delete s.clientIndex
            delete s.clientsIndexes
            // mais on garde clientOk et clientKo
            context.redirect(getBaseUrl() + getProp('afterClientsPage'))

          // ou redirect vers prochain client
          } else {
            ssoLog(`loginOnClients next avec ${s.user && s.user.nom}`)
            const client = clients[s.clientIndex]
            if (!client) throw new Error('Appel de loginOnClients sans client à appeler')
            if (!s.user) throw new Error('Appel de loginOnClients sans utilisateur en session')
            redirectToClient(context, client, s.user)
          }
          return
        }

        throw new Error('premier appel de loginOnClients sans user, ou perte de session')
      } catch (error) {
        console.error(error)
        errorCallback(context, error)
      }
    }

    const componentConfig = $settings.get('components.sesalabSso', {})
    const baseUrl = $settings.get('application.baseUrl')
    init(componentConfig, baseUrl)

    /**
     * @service $sesalabSsoServer
     */
    return {
      getClientsLogoutUrl,
      loginOnClients,
      setErrorCallback
    }
  })
}

/**
 * @typedef {Object} AuthServer
 * @property {string} baseUrl url absolue avec / de fin
 * @property {string} validate url relative du validate
 * @property {string} loginPage url relative du form d'authentification
 * @property {string} logoutPage url relative de la page qui fera toutes les déconnexions en ajax et affichera le résultat
 * @property {string} errorPage url relative de la page qui affichera une erreur
 * @property {string|RegExp|function} [ip] ip qui répond au validate
 *                                      peut être une ip (v4 ou v6)
 *                                      ou une RegExp
 *                                      ou une fct (qui doit renvoyer un booléen en reçevant une ip)
 */

/**
 * @typedef {Object} Client
 * @property {string} baseUrl url absolue avec / de fin
 * @property {string} login url vers laquelle rediriger, qui lancera le validate et redirigera vers le serveur
 * @property {string} logout url à appeler en ajax pour déconnecter
 * @property {string|RegExp|function} [ip] ip qui appellera le validate
 *                                      peut être une ip (v4 ou v6)
 *                                      ou une RegExp
 *                                      ou une fct (qui doit renvoyer un booléen en reçevant une ip)
 */