/**
* 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)
})
})
})
}