Source: component/controllerClient.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'

/**
 * Controleur pour gérer les routes du module sesalab-sso (SSO entre sesalab et sesatheque)
 * @controller controllerSesalabSso
 */

/**
 * @private
 * @type {$json}
 */
const $json = require('./json')
const request = require('request')
const { getAuthServer, getBaseUrl, loginCallback, logoutCallback } = require('./helperClient')
const { errorCallback } = require('./helper')
const sjtUrl = require('sesajstools/http/url')
const uuid = require('an-uuid')
const { getSidKey, xFerPrefix } = require('./common')
// la durée des sessions lassi (cf lassi:source/SessionStore.js)
const lassiSessionTtl = 24 * 3600

module.exports = function (component, prefix) {
  component.controller(prefix, function ($cache) {
    /**
     * Retourne le baseUrl trouvé en GET dans authBaseUrl (ajoute un slash de fin si besoin)
     * @private
     * @param {Context} context
     * @returns {string}
     * @throws {Error} si pas trouvé
     */
    function getAuthBaseUrl (context) {
      let authBaseUrl = context.get.authBaseUrl
      if (!authBaseUrl) throw new Error('Paramètres invalides')
      if (authBaseUrl.substr(-1) !== '/') authBaseUrl += '/'
      return authBaseUrl
    }

    /**
     * Redirige vers le serveur d'authentification avec une erreur dans l'url
     * @private
     * @param {Context} context
     * @param {AuthServer} authServer
     * @param {string} errorMessage
     */
    function redirectError (context, authServer, errorMessage) {
      let urlRedirect = authServer.baseUrl + authServer.errorPage
      urlRedirect += urlRedirect.indexOf('?') === -1 ? '?' : '&'
      urlRedirect += 'error=' + encodeURIComponent(errorMessage)
      context.redirect(urlRedirect)
    }

    /**
     * Renvoie vers le serveur d'authentification
     * @private
     * @param {Context} context
     * @param {string} retour
     */
    function redirectOkToAuthServer (context, retour) {
      retour += (retour.indexOf('?') === -1) ? '?' : '&'
      retour += 'success=true'
      context.redirect(retour)
    }

    const $settings = lassi.service('$settings')
    const baseUrl = $settings.get('application.baseUrl')
    // const baseId = $settings.get('application.baseId', 'unknown')
    const sessionName = $settings.get('$rail.session.name', 'connect.sid')

    /**
     * Connexion propagée ici, avec le token du serveur d'authentification dans l'url
     * - appelle le validate du serveur
     * - logue le user ici (génère un authToken, et redirige vers cette même url si on a pas encore de cookie de session)
     * - redirige vers l'expéditeur
     * @route GET /sesalabSso/login
     */
    this.get('login', function (context) {
      try {
        const authBaseUrl = getAuthBaseUrl(context)
        // le token que nous passe le serveur
        const serverToken = context.get.token
        // notre token, associé à la session ici, que le serveur devra mettre dans un header Authorization
        // pour les appels de notre api.
        // Il le récupère dans notre appel du validate, lui associe un token temporaire qu'il nous renvoie
        // dans la réponse du validate, et qu'on lui ajoute dans l'url de retour
        // (on veut pas mettre le vrai token dans l'url,
        // et on ne peut pas poster avec une redirection)
        const authToken = uuid()
        if (!serverToken) throw new Error('Paramètres invalides')
        const authServer = getAuthServer(authBaseUrl)
        if (!authServer) throw new Error('Serveur d’authentification ' + authBaseUrl + ' inconnu')
        let urlRetour = context.get.retour
        if (!urlRetour) throw new Error('url de retour manquante')
        // on a tout ce qu'il faut, mais si on a pas encore le cookie de session, probable au 1er appel,
        // on ne peut pas encore le récupérer ici car express ne l'a pas encore mis,
        // context.response.get('set-cookie') renvoie undefined
        // on peut pas prendre context.session.id car le cookie est bcp plus long
        // Dans ce cas on est obligé de faire un redirect pour récupérer ici le cookie qu'express va affecter
        // et qui sera renvoyé par le navigateur
        const re = new RegExp(sessionName + '=([^; ]+)')
        const matches = re.exec(context.request.headers.cookie)
        let sid
        if (matches && matches[1]) sid = matches[1]
        if (context.get.cookied && !sid) throw new Error('pas de session, impossible de continuer')
        if (!sid) return context.redirect(context.request.originalUrl + '&cookied=1')

        // on a notre cookie de session tel qu'envoyé par le navigateur et on peut continuer
        urlRetour = authBaseUrl + urlRetour
        // faut ajouter le serverToken pour que le serveur l'associe dans afterLogin au token
        // qu'on lui a passé au validate
        if (urlRetour.indexOf('?') === -1) urlRetour += '?'
        else urlRetour += '&'
        urlRetour += 'token=' + serverToken
        const options = {
          uri: sjtUrl.complete(authBaseUrl + authServer.validate, { token: serverToken }),
          json: true,
          timeout: 3000,
          headers: {
            Authorization: xFerPrefix + ' ' + authToken,
            Origin: baseUrl
          }
        }
        // appli cliente appelle authServer pour récupérer le user
        request(options, function (error, response, data) {
          // console.log('le validate renvoie', error, data)
          if (error || !data || !data.user) {
            // on loggue l'erreur ici
            if (error) console.error(error)
            else console.error(`Le validate de ${authBaseUrl} avec authToken ${authToken} ne renvoie pas de user mais`, data)
            // et on renvoie l'utilisateur avec une erreur
            redirectError(context, authServer, `Impossible de propager l’authentification vers ${getBaseUrl()} car le serveur d'authentification ${authBaseUrl} ne lui renvoie pas la validation attendue`)
          } else {
            // on a un user, on le passe à loginCallback pour l'authentifier ici
            loginCallback(context, data.user, function (error) {
              if (error) {
                console.error(error)
                redirectError(context, authServer, `Impossible de propager l'authentification sur ${getBaseUrl()}`)
              } else {
                // on met authBaseUrl en session pour le logout (faudra l'appeler)
                context.session.authBaseUrl = authBaseUrl
                // le token que l'appli faisant tourner le serveur d'authentification pourra utiliser pour
                // appeler l'api de ce client (à priori pas la peine de le mettre en session, sinon pour vérifier)
                context.session.authToken = authToken
                // et on associe sessionId à ce authToken qui nous sera ensuite passé en header Authorization
                const cacheKey = getSidKey(authToken)
                $cache.set(cacheKey, sid, lassiSessionTtl, function (error) {
                  if (error) return errorCallback(error)
                  redirectOkToAuthServer(context, urlRetour)
                })
              }
            })
          }
        })
      } catch (error) {
        // on a pas de authServer vers lequel renvoyer, on affiche l'erreur ici
        errorCallback(context, error)
      }
    })

    /**
     * Réponse en json à un appel xhr de déconnexion (de l'utilisateur sur son serveur d'authentification)
     * @route GET /sesalabSso/logout
     */
    this.get('logout', function (context) {
      logoutCallback(context, function (error) {
        if (error) $json.sendError(context, error)
        else $json.sendOk(context)
      })
    })
  })
}