CESI - Exo Microbloggr Solution - Authentification fonctionnelle

This commit is contained in:
wpetit 2018-11-28 14:47:09 +01:00
parent 68ccdca3a4
commit eaa659f3c6
31 changed files with 491 additions and 213 deletions

View File

@ -1,14 +1,76 @@
version: '2'
version: '2.2'
services:
gateway:
build:
context: containers/gateway
context: services/gateway
args:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- http_proxy=${HTTP_PROXY}
- https_proxy=${HTTP_PROXY}
links:
- home_page
- login_page
- logout
ports:
- 8080:80
home_page:
build:
context: containers/home_page
context: services/home_page
args:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- http_proxy=${HTTP_PROXY}
- https_proxy=${HTTP_PROXY}
environment:
SESSION_SECRET: "absolutly_not_secret"
links:
- redis
volumes:
- ./services/home_page/src:/app/src:ro
login_page:
build:
context: containers/login_page
context: services/login_page
args:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- http_proxy=${HTTP_PROXY}
- https_proxy=${HTTP_PROXY}
links:
- users
- redis
volumes:
- ./services/login_page/src:/app/src:ro
environment:
SESSION_SECRET: "absolutly_not_secret"
logout:
build:
context: services/logout
args:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- http_proxy=${HTTP_PROXY}
- https_proxy=${HTTP_PROXY}
volumes:
- ./services/logout/src:/app/src:ro
environment:
SESSION_SECRET: "absolutly_not_secret"
users:
build:
context: services/users
args:
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- http_proxy=${HTTP_PROXY}
- https_proxy=${HTTP_PROXY}
volumes:
- ./services/users/src:/app/src:ro
redis:
image: redis:5.0-alpine

View File

@ -1,4 +1,4 @@
FROM alpine:3.2
FROM alpine:3.7
RUN apk add --no-cache nginx
COPY microbloggr.conf /etc/nginx/microbloggr.conf

View File

@ -28,6 +28,10 @@ http {
proxy_pass http://login_page:8080/;
}
location /logout {
proxy_pass http://logout:8080/;
}
# location /profile {
# proxy_pass http://profile_page:8080/;
# }

View File

@ -2,9 +2,10 @@ FROM alpine:3.7
RUN apk add --no-cache nodejs
COPY app /app
COPY ./ /app
WORKDIR /app
RUN npm install --production
RUN npm install -g nodemon
CMD ["node", "server.js"]
CMD ["nodemon", "src/server.js"]

View File

@ -0,0 +1,69 @@
{
"name": "login_page",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"morgan": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz",
"integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==",
"requires": {
"basic-auth": "~2.0.0",
"debug": "2.6.9",
"depd": "~1.1.2",
"on-finished": "~2.3.0",
"on-headers": "~1.0.1"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"on-headers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
"integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c="
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
}

View File

@ -12,6 +12,8 @@
"ejs": "^2.5.7",
"express": "^4.16.2",
"express-session": "^1.15.6",
"connect-redis": "^3.4.0",
"morgan": "^1.9.1",
"request": "^2.83.0",
"request-promise-native": "^1.0.5"
}

View File

@ -1,11 +1,17 @@
const express = require('express');
const session = require('express-session');
var RedisStore = require('connect-redis')(session);
const app = express();
const morgan = require('morgan');
app.set('view engine', 'ejs');
app.set('views', __dirname + '/views');
app.use(morgan('combined'));
app.use(session({
secret: 'AbsolutlyNotSecret', // Ce secret doit être partagé par tous les microservices *_page
store: new RedisStore({host: 'redis'}),
secret: process.env.SESSION_SECRET, // Ce secret doit être partagé par tous les microservices *_page
cookie: { maxAge: 60000 },
resave: false,
saveUninitialized: true,
}));
@ -20,10 +26,10 @@ app.get('/', (req, res) => {
// On vérifie si un utilisateur a été associé à la session HTTP
if (req.session.user) {
// Si oui, on affichage la page d'accueil
return res.render('index');
return res.render('index', { user: req.session.user });
} else {
// Si non, on redirige l'utilisateur vers la page de login
return res.redirect(301, '/login');
return res.redirect(302, '/login');
}
});

View File

@ -4,6 +4,10 @@
<nav class="main">
<h1 class="brand">MicroBloggr</div>
</nav>
<div class="content">
Bienvenue <%= user.username; %> !
<a href="/logout">Se déconnecter</a>
</div>
</div>
</body>
</html>

View File

@ -2,9 +2,10 @@ FROM alpine:3.7
RUN apk add --no-cache nodejs
COPY app /app
COPY ./ /app
WORKDIR /app
RUN npm install --production
RUN npm install -g nodemon
CMD ["node", "server.js"]
CMD ["nodemon", "src/server.js"]

View File

@ -1,91 +0,0 @@
const express = require('express');
const app = express();
const session = require('express-session');
const bodyParser = require('body-parser');
const request = require('request-promise-native');
app.set('view engine', 'ejs');
app.use(session({
secret: 'AbsolutlyNotSecret', // Ce secret doit être partagé par tous les microservices *_page
resave: false,
saveUninitialized: true,
}));
// On utilise le module "body-parser"
// pour faciliter la désérialisation des données
// du formulaire
app.use(bodyParser.urlencoded({extended: false}));
// GET /
// Affiche la page de login
app.get('/', (req, res) => {
res.render('index', { error: null });
});
// POST /
// Récupère les données du formulaire de login
app.post('/', (req, res) => {
const form = req.body;
// Si le formulaire ne contient pas les données attendues
// on renvoie un code HTTP 400 pour indiquer une requête invalide.
if(!form.username || !form.password) {
return res.render('index', { error: 'Identifiant ou mot de passe manquant.'});
}
// On interroge le microservices "users"
// pour récupérer l'utilisateur associé à l'identifiant
// fourni dans le formulaire
findUserByName(form.username)
.then(user => {
// Si l'utilisateur n'existe pas, on affiche un message d'erreur
if(!user) {
return res.render('index', { error: 'Identifiant invalide !'});
}
// Si le mot de passe de l'utilisateur correspond à celui enregistré sur le
// microservice "users", on attache l'identifiant de l'utilisateur à la session
// HTTP puis on le redirige sur la page d'accueil
if(user.password === form.password) {
req.session.user = user;
return res.redirect(302, '/');
} else {
// Sinon, on affiche un message d'erreur
return res.render('index', { error: 'Mot de passe invalide !'});
}
})
.catch(err => {
// Une erreur s'est produite en essayant de contacter le microservices "users"
// On affiche un message d'erreur à l'utilisateur
return res.render('index', {
error: 'Une erreur s\'est produite pendant le processus d\'authentification.'
});
})
;
});
app.listen(8080, () => console.log('listening on port 8080'));
// Récupère un utilisateur via son identifiant
// sur le microservice "users"
function findUserByName(username) {
return request({
uri: `http://users:8080/users/${username}`,
json: true
})
.catch(err => {
// Si le microservice "users" répond avec un code HTTP 404
// c'est que l'utilisateur n'existe pas
if (err.response.statusCode === 404) {
return null;
}
throw err;
})
;
}

View File

@ -0,0 +1,118 @@
{
"name": "login_page",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"connect-redis": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-3.4.0.tgz",
"integrity": "sha512-YKPSO9tLwzUr8jzhsGMdSJUxevWrDt0ggXRcTMb+mtnJ/vWGlWV7RC4VUMgqvZv3uTGDFye8Bf7d6No0oSVkOQ==",
"requires": {
"debug": "^4.0.1",
"redis": "^2.8.0"
},
"dependencies": {
"debug": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz",
"integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"double-ended-queue": {
"version": "2.1.0-0",
"resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
"integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"morgan": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz",
"integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==",
"requires": {
"basic-auth": "~2.0.0",
"debug": "2.6.9",
"depd": "~1.1.2",
"on-finished": "~2.3.0",
"on-headers": "~1.0.1"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"on-headers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
"integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c="
},
"redis": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
"integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
"requires": {
"double-ended-queue": "^2.1.0-0",
"redis-commands": "^1.2.0",
"redis-parser": "^2.6.0"
}
},
"redis-commands": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.4.0.tgz",
"integrity": "sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw=="
},
"redis-parser": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz",
"integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs="
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
}

View File

@ -10,9 +10,11 @@
"license": "ISC",
"dependencies": {
"body-parser": "^1.18.2",
"connect-redis": "^3.4.0",
"ejs": "^2.5.7",
"express": "^4.16.2",
"express-session": "^1.15.6",
"morgan": "^1.9.1",
"request": "^2.83.0",
"request-promise-native": "^1.0.5"
}

View File

@ -0,0 +1,83 @@
const express = require('express');
const app = express();
const session = require('express-session');
var RedisStore = require('connect-redis')(session);
const bodyParser = require('body-parser');
const request = require('request-promise-native');
const morgan = require('morgan');
app.set('view engine', 'ejs');
app.set('views', __dirname + '/views');
app.use(morgan('combined'));
app.use(session({
store: new RedisStore({host: 'redis'}),
secret: process.env.SESSION_SECRET, // Ce secret doit être partagé par tous les microservices *_page
cookie: { maxAge: 60000 },
resave: false,
saveUninitialized: true,
}));
// On utilise le module "body-parser"
// pour faciliter la désérialisation des données
// du formulaire
app.use(bodyParser.urlencoded({extended: false}));
// GET /
// Affiche la page de login
app.get('/', (req, res) => {
res.render('index', { error: null });
});
// POST /
// Récupère les données du formulaire de login
app.post('/', (req, res) => {
const form = req.body;
// Si le formulaire ne contient pas les données attendues
// on renvoie un code HTTP 400 pour indiquer une requête invalide.
if(!form.username || !form.password) {
return res.render('index', { error: 'Identifiant ou mot de passe manquant.'});
}
// On interroge le microservices "users"
// pour récupérer l'utilisateur associé à l'identifiant
// fourni dans le formulaire
authenticateUser(form.username, form.password)
.then(result => {
// Si l'utilisateur n'existe pas, on affiche un message d'erreur
if(!result.ok) {
return res.render('index', { error: result.error });
}
// Si le microservice "users" renvoie "ok", alors cela signifie
// que le mot de passe est valide
req.session.user = result.user;
return res.redirect(303, '/');
})
.catch(err => {
console.error(err);
return res.end(500).send(err.message);
})
;
});
app.listen(8080, () => console.log('listening on port 8080'));
// Authentifie un utilisateur via son couple identifiant/mot de passe
// sur le microservice "users"
function authenticateUser(username, password) {
return request({
uri: `http://users:8080/auth`,
method: 'POST',
body: {
username, password
},
json: true
})
;
}

View File

@ -2,9 +2,10 @@ FROM alpine:3.7
RUN apk add --no-cache nodejs
COPY app /app
COPY ./ /app
WORKDIR /app
RUN npm install --production
RUN npm install -g nodemon
CMD ["node", "server.js"]
CMD ["nodemon", "src/server.js"]

View File

@ -0,0 +1,17 @@
{
"name": "logout",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"connect-redis": "^3.4.0",
"express-session": "^1.15.6",
"express": "^4.16.2",
"morgan": "^1.9.1"
}
}

View File

@ -0,0 +1,25 @@
const express = require('express');
const app = express();
const session = require('express-session');
var RedisStore = require('connect-redis')(session);
const morgan = require('morgan');
app.use(morgan('combined'));
app.use(session({
store: new RedisStore({host: 'redis'}),
secret: process.env.SESSION_SECRET, // Ce secret doit être partagé par tous les microservices accédant à la session
cookie: { maxAge: 60000 },
resave: false,
saveUninitialized: true,
}));
// GET /
app.get('/', (req, res) => {
req.session.destroy(err => {
if (err) return res.status(500).send(err.stack);
return res.status(303).redirect("/");
});
});
app.listen(8080, () => console.log('listening on port 8080'));

View File

@ -1,102 +0,0 @@
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const sessions = {};
let sessionCounter = 0;
// On utilise le middleware "body-parser"
// pour désérialiser automatiquement
// les objets JSON potentiellement
// fournis par les requêtes
app.use(bodyParser.json());
// GET /sessions
// Permet de récupérer la liste des sessions en cours
app.get('/sessions', (req, res) => {
res.json(sessions);
});
// GET /sessions/:id
// Permet de récupérer une session via son identifiant
app.get('/sessions/:id', (req, res) => {
// On récupère l'identifiant de session
// depuis l'URL
const sessionId = req.params.id;
// On vérifie que la session existe.
// Si ce n'est pas le cas, on retourne un code
// HTTP 404 pour indiquer que la ressource
// n'existe pas
if (!sessions.hasOwnProperty(sessionId)) {
return res.status(404).end('Not found.');
}
// On retourne la session sérialisée en JSON
return res.json(sessions[sessionId]);
})
// POST /sessions
// Body: {"user": <user_id> }
// Permet de créer une nouvelle session pour l'utilisateur donné
app.post('/sessions', (req, res) => {
const payload = req.body;
// On valide la présence de l'attribut "user"
// sur les données fournies.
// Si il n'est pas présent, on retourne un code HTTP
// 400 pour indiquer que la requête est invalide.
if (!payload.user) {
return res.status(400).end('Bad request.');
}
// On créait une nouvelle session
const sess = {
user: payload.user,
id: getSessionId(),
};
// On la "stocke" dans notre registre
sessions[session.id] = sess;
// On retourne la session
// nouvellement créée
res.json(sess);
})
// DELETE /sessions/:id
// Permet de supprimer une session dans le registre via son identifiant
app.delete('/sessions/:id', (req, res) => {
// On récupère l'identifiant de session
// depuis l'URL
const sessionId = req.params.id;
// On vérifie que la session existe.
// Si ce n'est pas le cas, on retourne un code
// HTTP 404 pour indiquer que la ressource
// n'existe pas
if (!sessions.hasOwnProperty(sessionId)) {
return res.status(404).end('Not found.');
}
delete sessions[sessionId];
// On retourne un code HTTP 204 pour indiquer que l'opération
// s'est déroulée correctement mais qu'aucun "contenu" n'est disponible
return res.status(204);
});
app.listen(8080, () => console.log('listening on port 8080'));
function getSessionId() {
return `${Date.now()}-${sessionCounter++}`;
}

View File

@ -0,0 +1,11 @@
FROM alpine:3.7
RUN apk add --no-cache nodejs
COPY ./ /app
WORKDIR /app
RUN npm install --production
RUN npm install -g nodemon
CMD ["nodemon", "src/server.js"]

View File

@ -1,8 +1,8 @@
{
"name": "sessions",
"name": "users",
"version": "1.0.0",
"description": "",
"main": "server.js",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"body-parser": "^1.18.2",
"express": "^4.16.2"
"express": "^4.16.2",
"morgan": "^1.9.1"
}
}

View File

@ -0,0 +1,44 @@
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const morgan = require('morgan');
const users = require('./users');
app.use(morgan('combined'));
// On utilise le module "body-parser"
// pour faciliter la désérialisation des données
// des appels à l'API
app.use(bodyParser.json());
// POST /auth
// Effectue une authentification pour un couple d'identifiants donné
app.post('/auth', (req, res) => {
const credentials = req.body;
if(!credentials.username || !credentials.password) {
return res.status(200).send({
ok: false,
error: "Identifiants invalides.",
});
}
const user = users[credentials.username];
if (!user || user.password !== credentials.password) {
return res.status(200).send({
ok: false,
error: "Identifiants invalides.",
});
}
return res.status(200).send({
ok: true,
user,
});
});
app.listen(8080, () => console.log('listening on port 8080'));

View File

@ -0,0 +1,17 @@
// Liste des utilisateurs définie statiquement
// Dans une situation réelle, ces comptes seraient stockés
// en BDD et les mots de passe hachés/salés.
module.exports = {
admin: {
username: 'admin',
password: 'admin'
},
user1: {
username: 'user1',
password: 'user1'
},
user2: {
username: 'user2',
password: 'user2'
}
};