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