/** * @file Initialise et configure le serveur Express sur lequel tourne le back. * * Inclut les middlewares des packages utilisés: apollo-server, passportjs, morgan... * et branche nos propres middleware : l'API GraphQL et l'interface admin du backend (adminview). * Pour comprendre ce que fait chaque package, se référer à leur page sur https://www.npmjs.com. * * On peut considérer que les app.use (et app.get et app.post) sont pattern-matchés et * exécutés séquentiellement. http://expressjs.com/en/guide/using-middleware.html * * @author manifold, kadabra, ofacklam */ import express from 'express'; import bodyParser from 'body-parser'; // packages pour graphql import { ApolloServer } from 'apollo-server-express'; import schema from './graphql/schema'; // definition du schéma et des resolvers // l'interface admin du backend (adminview) import router from './adminview/admin_router'; // packages pour l'authentification import passport from 'passport'; import './config_passport'; // configure passport, pour l'authentification ldap et pour comment gérer les sessions (serializeUser/deserializeUser) import expressSession from 'express-session'; import cors from 'cors'; // HTTP request logger import morgan from 'morgan'; // packages pour pouvoir importer des fichiers de config import path from 'path'; // config des paramètres de connexion au LDAP import { ldapConfig } from './ldap/internal/config'; //loads environment variables from (hidden) .env file import dotenv from 'dotenv'; dotenv.config(); /** * @desc Création de l'application Express et setup de middlewares basiques * ======================================================================== */ const app = express(); // parse incoming HTTP request bodies, available under the req.body property app.use(bodyParser.json()); //parses bodies of media type "application/json" app.use(bodyParser.urlencoded({ //parses bodies of media type "application/x-www-form-urlencoded" extended: true //use qs library (see https://www.npmjs.com/package/body-parser#bodyparserurlencodedoptions) })); // ne *pas* inclure de header HTTP publicisant que l'application tourne sous Express app.disable('x-powered-by'); // use morgan (HTTP request logger middleware) app.use(morgan('dev')); const FRONTEND_SERVER_URL = process.env.FRONTEND_SERVER_URL; // Options de configuration pour le _middleware_ `cors`. // CORS = Cross Origin Resource Sharing const corsOptions = { //configures the "Access-Control-Allow-Origin" CORS header, declaring that sigma-back wants to make resources accessible to that site (and that site only) origin: FRONTEND_SERVER_URL, //configures the "Access-Control-Allow-Credentials" CORS header, allowing cookies to be included on cross-origin requests credentials: true }; app.use(cors(corsOptions)); // respond to "GET /favicon.ico" requests // favicon middleware is placed near the top of the middleware stack, to answer favicon requests faster, as they are relatively frequent // (plus, they should not have to trigger any authentication middleware) import favicon from 'serve-favicon'; // tres tres important :p app.use(favicon(path.resolve(__dirname, '..', 'assets', 'favicon.ico'))); /** * @desc Authentification de la requête contre le session-store (cookie) * ===================================================================== * Si la requête possède un cookie, on regarde dans la session s'il s'agit d'un cookie valide. * Si oui, on récupère l'uid de l'utilisateur correspondant dans la session. * L'uid qu'on a trouvé est stocké dans req.user, et sera passé à GraphQL, dans le context. * (Si non, on passera quand même dans context un uid spécial signifiant que l'utilisateur n'est pas authentifié. cf la définition de context, plus loin) * * NB : la session ne contient que les correspondances cookie / uid, et rien d'autre. * passport.serializeUser et .deserializeUser servent en théorie à traduire * * https://stackoverflow.com/questions/22052258/what-does-passport-session-middleware-do * * On a un petit peu la meme demarche que l'Option 1 de * https://blog.apollographql.com/a-guide-to-authentication-in-graphql-e002a4039d1 */ /** * it is also here that we define parameters for *session store*. https://redislabs.com/blog/cache-vs-session-store/ * configure this right, as express-session docs warns that the default config is not good for prod!! * @todo [critical] configure express-session (session store and other options) * @todo choose a session secret and where to store it * https://www.npmjs.com/package/express-session * Sur l'utilité des flags dans les cookies : https://www.information-security.fr/securite-sites-web-lutilite-flags-secure-httponly/ */ // load data from the session identified by the cookie (if one exists), into req.session // on ne manipulera pas req.session directement, on laisser toujours passport le faire pour nous app.use(expressSession({ secret: "asdfjklkjfdsasdfjklkljfdsa", resave: false, // 'resave: true' forces the session to be saved back to the session store (false OK if store implements touch()) rolling: false, // 'rolling: true' resets expiration at every response saveUninitialized: true, // 'saveUninitialized: true' forces empty sessions to also be stored in a cookie //store: ,// default is NOT good => @ofacklam est d'avis d'utiliser 'connect-session-knex', comme ca on peut le plug directement dans notre BDD. cookie: { maxAge: 3600000, // Une heure avant expiration du cookie (en millisecondes) //secure: true, // Le cookie ne peut transiter qu'en HTTPS. ATTENTION : If you have your node.js behind a proxy and are using secure: true, you need to set "trust proxy" in express httpOnly: true } })); app.use(passport.initialize()); //passport.session(): load the user object onto req.user if a serialised user object was found in the req.session // aucun effet sur les requetes sans cookie ou les requetes avec cookie expired, // puisqu'elles ne sont pas dans req.session, puisque non-reconnues par app.use(expressSession(...)) //(see http://toon.io/understanding-passportjs-authentication-flow/) app.use(passport.session(), (req, res, next) => { let user = req.user ? req.user.uid : "none"; console.log(`passport.session(): found user: ${user}, authenticated: ${req.isAuthenticated()}`); next(); }); /** * @desc Répondre aux requêtes de connexion par LDAP (venant du front) * =================================================================== * i.e. endpoint for frontend's authentication requests (logins through adminview/avlogin are caught by the router in admin_router.js) * i.e. quand l'utilisateur submit le formulaire de login avec ses identifiants/mdp dans le front * * @todo gérer le cas où une requête à /login est reçue, mais où cette requête contient un cookie valide * @todo vérifier qu'on ne fallthrough pas, i.e. qu'on renvoie une response et qu'on ne trigger pas les middlewares suivants une fois celui-ci terminé * @todo rassify */ //with custom callback: //http://www.passportjs.org/docs/authenticate/#custom-callback // http://toon.io/understanding-passportjs-authentication-flow/ app.post('/login', (req, res, next) => { console.log("Received an authentication request to /login"); passport.authenticate('ldapauth', (err, user, info) => { console.log("| Entering passport.authenticate('ldapauth', - ) callback"); // If an exception occurred if (err) { console.log("| Error when trying to passport.authenticate with ldapauth"); console.log(err); return res.status(err.status).json({ message: "Exception raised in backend process during authentication: " + err, authSucceeded: false }); // return next(err); // handle error? or drop request and answer with res.json()? } // If authentication failed, user will be set to false if (!user) { console.log("| Authentication failed, passport.authenticate did not return a user. "); return res.status(401).json({ message: "Authentication failed: " + info.message, authSucceeded: false }); } req.login(user, (err) => { // If an exception occurred at login if (err) { console.log("| Error when trying to req.login in callback in passport.authenticate('ldapauth', - )"); console.log(err); return res.status(err.status).json({ message: "Exception raised in backend process during login: " + err, authSucceeded: false }); // return next(err); // handle error? or drop request and answer with res.json()? } // If all went well console.log("| Authentication succeeded! :)"); // passport.authenticate automatically includes a Set-Cookie HTTP header in // the response. The JSON body is just to signal the frontend that all went well return res.status(200).json({ message: 'Authentication succeeded', authSucceeded: true }); }); })(req, res, next); }); /** * @desc Servir l'API GraphQL à proprement parler * ============================================== */ // Define GraphQL request's context object, through a callback, with authorization. // See: https://www.apollographql.com/docs/apollo-server/features/authentication.html import { Context } from './graphql/typeDefs/queries'; import { AuthorizationModel } from './graphql/models/authorization'; import { UserModel } from './graphql/models/userModel'; import { GroupModel } from './graphql/models/groupModel'; import { MessageModel } from './graphql/models/messageModel'; import { RequestModel } from './graphql/models/requestModel'; const context = async ({ req }): Promise<Context> => { // set a special uid for non-authenticated requests // /!\ FOR DEVELOPMENT ONLY: use the one in the ldap_credentials.json file (imported by config.ts) // for production, replace with a "publicUser" or "notLoggedInUser" or something. let uid = AuthorizationModel.PUBLICUSER; console.log("Responding to graphql request..."); console.log(` | User: ${req.user ? req.user.uid : "none"} | Authenticated: ${req.isAuthenticated()} `.trim()); if(req.isAuthenticated() && req.user) { console.log("graphql API is receiving a request from an authenticated user! \\o/"); try { uid = req.user.uid; } catch (err) { console.log("Error: req is authenticated, but pb when trying to extract uid from req.user. Probably user was either not serialized or not deserialized properly"); console.log(err); } } console.log(`Constructing context with uid = ${uid}`); let c = { request: req, user: { uid: uid }, models: { auth: await AuthorizationModel.create(uid), user: new UserModel(uid), group: new GroupModel(uid), message: new MessageModel(uid), request: new RequestModel(uid) } }; console.log("finished constructing context"); return c; }; const server = new ApolloServer({ ...schema, context, playground: { settings: { "editor.theme": "dark", "editor.cursorShape": 'line', "request.credentials": 'include' } } }); // path defaults to '/graphql' //server.applyMiddleware({ app, cors: corsOptions }); <-- TODO: is this necessary? server.applyMiddleware({ app }); /** * @desc Servir l'interface admin du backend ("adminview") * ======================================================= * On redirige vers la "mini-application" adminview. * Il s'agit en fait d'un express.Router, qui sert a creer un "sous-middleware stack". * Par cette interface admin, on a accès : * - à une API REST fait-maison (peut-être encore à débugger) * - à GraphQL Voyager, un package qui permet d'afficher une représentation sous forme de graphe du schéma GraphQL. */ // servir les fichiers statiques (e.g. images) rendant l'interface jolie app.use('/assets', express.static(path.resolve(__dirname, '../', 'assets'))); // setup the 'pug' view engine. the adminview "sub-app" will inherit this. https://expressjs.com/en/4x/api.html#app.set //console.log("Express app is running at (__dirname) ", __dirname); //console.log("Express app is running at (full path) ", path.resolve(__dirname));let viewpath = path.resolve(__dirname, 'adminview', 'views'); let viewpath = path.resolve(__dirname, 'adminview', 'views'); app.set('views', viewpath); app.set('view engine', 'pug'); app.use('/adminview', router); // catches and resolves HTTP requests to paths '/adminview/*' // also redirect other HTTP GET requests to the adminview router app.get('/', function (req, res) { res.redirect('/adminview'); }); /** * @desc Catch-all * =============== * Normalement aucune requête ne devrait arriver au dernier middleware : * les requêtes GraphQL doivent être adressées à /graphql, * les requêtes de connexion du frontend doivent être adressées à /login, * les requêtes vers l'adminview doivent être adressées à /adminview/* * Donc c'est qu'il y a un problème quelque part. On le log et on répond 500 Internal Server Error (ou 400 Bad Request?). * * @todo faire un loggage plus propre que ça... */ app.use('/', (req, res) => { console.log("Erreur: une requête est arrivée au dernier middleware"); res.status(500).send({ error: "unexpectedly triggered catchall" }); }); export default app;