Merge pull request #143 from ets-cfuhrman-pfe/modularise-auth-methods

Modularise auth methods
This commit is contained in:
Gabriel Moisan Matte 2024-10-01 13:32:28 -04:00 committed by GitHub
commit 975c88c894
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 542 additions and 149 deletions

View file

@ -23,13 +23,16 @@ import Header from './components/Header/Header';
import Footer from './components/Footer/Footer';
import ApiService from './services/ApiService';
import OAuthCallback from './pages/AuthSelection/AuthCallback';
const handleLogout = () => {
ApiService.logout();
};
const isLoggedIn = () => {
return ApiService.isLoggedIn();
const test = ApiService.isLoggedIn();
console.log("App.tsx: " + test);
return test;
};
function App() {
@ -71,6 +74,9 @@ function App() {
{/* Pages authentification sélection */}
<Route path="/auth-selection" element={<AuthSelection />} />
{/* Pages authentification sélection */}
<Route path="/oauth/callback" element={<OAuthCallback />} />
</Routes>
</main>
</div>

View file

@ -1,4 +1,4 @@
import { useNavigate } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import * as React from 'react';
import './header.css';
import { Button } from '@mui/material';
@ -32,6 +32,14 @@ const Header: React.FC<HeaderProps> = ({ isLoggedIn, handleLogout }) => {
Logout
</Button>
)}
{!isLoggedIn() && (
<div className="auth-selection-btn">
<Link to="/auth-selection">
<button className="auth-btn">Connexion</button>
</Link>
</div>
)}
</div>
);
};

View file

@ -0,0 +1,27 @@
import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import apiService from '../../services/ApiService';
const OAuthCallback: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
useEffect(() => {
const searchParams = new URLSearchParams(location.search);
const user = searchParams.get('user');
if (user) {
// Save user data to localStorage or sessionStorage
apiService.saveToken(user);
// Navigate to the dashboard or another page
navigate('/');
} else {
navigate('/auth-selection');
}
}, [location, navigate]);
return <div>Loading...</div>;
};
export default OAuthCallback;

View file

@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import './authselection.css';
import SimpleLogin from './SimpleLogin';
const AuthSelection: React.FC = () => {
const [simpleLoginData, setSimpleLoginData] = useState({ username: '', password: '' });
const [authData, setAuthData] = useState<any>(null); // Stocke les données d'auth
const navigate = useNavigate();
@ -22,17 +22,6 @@ const AuthSelection: React.FC = () => {
fetchAuthData(); // Appel de la fonction pour récupérer les données
}, []);
const handleSimpleLoginChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setSimpleLoginData((prev) => ({ ...prev, [name]: value }));
};
const handleSimpleLoginSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Logique d'authentification pour Simple Login
console.log('Simple Login Data:', simpleLoginData);
};
const handleAuthLogin = (provider: string) => {
window.location.href = 'http://localhost:3000/api/auth/' + provider;
};
@ -44,25 +33,7 @@ const AuthSelection: React.FC = () => {
{/* Formulaire de connexion Simple Login */}
{authData && authData['simple-login'] && (
<div className="form-container">
<form onSubmit={handleSimpleLoginSubmit}>
<input
type="text"
name="username"
placeholder="Nom d'utilisateur"
value={simpleLoginData.username}
onChange={handleSimpleLoginChange}
required
/>
<input
type="password"
name="password"
placeholder="Mot de passe"
value={simpleLoginData.password}
onChange={handleSimpleLoginChange}
required
/>
<button type="submit">Se connecter</button>
</form>
<SimpleLogin/>
</div>
)}

View file

@ -0,0 +1,94 @@
import { useNavigate, Link } from 'react-router-dom';
// JoinRoom.tsx
import React, { useEffect, useState } from 'react';
import './simpleLogin.css';
import { TextField } from '@mui/material';
import LoadingButton from '@mui/lab/LoadingButton';
import LoginContainer from '../../components/LoginContainer/LoginContainer'
import ApiService from '../../services/ApiService';
const SimpleLogin: React.FC = () => {
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [connectionError, setConnectionError] = useState<string>('');
const [isConnecting] = useState<boolean>(false);
useEffect(() => {
return () => {
};
}, []);
const login = async () => {
const result = await ApiService.login(email, password);
if (result != true) {
setConnectionError(result);
return;
}
else {
navigate("/")
}
};
return (
<LoginContainer
title=''
error={connectionError}>
<TextField
label="Email"
variant="outlined"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Nom d'utilisateur"
sx={{ marginBottom: '1rem' }}
fullWidth
/>
<TextField
label="Mot de passe"
variant="outlined"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Nom de la salle"
sx={{ marginBottom: '1rem' }}
fullWidth
/>
<LoadingButton
loading={isConnecting}
onClick={login}
variant="contained"
sx={{ marginBottom: `${connectionError && '2rem'}` }}
disabled={!email || !password}
>
Login
</LoadingButton>
<div className="login-links">
<Link to="/teacher/resetPassword">
Réinitialiser le mot de passe
</Link>
<Link to="/teacher/register">
Créer un compte
</Link>
</div>
</LoginContainer>
);
};
export default SimpleLogin;

View file

@ -6,13 +6,6 @@ import { Link } from 'react-router-dom';
const Home: React.FC = () => {
return (
<div className="page">
<div className="auth-selection-btn">
<Link to="/auth-selection">
<button className="auth-btn">Connexion</button>
</Link>
</div>
<div className="btn-container">
<Link to="/student/join-room" className="student-btn">

View file

@ -32,7 +32,7 @@ class ApiService {
}
// Helpers
private saveToken(token: string): void {
public saveToken(token: string): void {
const now = new Date();
const object = {

View file

@ -3,14 +3,18 @@ version: '3'
services:
frontend:
image: fuhrmanator/evaluetonsavoir-frontend:latest
build:
context: ./server
dockerfile: Dockerfile
container_name: frontend
ports:
- "5173:5173"
restart: always
backend:
image: fuhrmanator/evaluetonsavoir-backend:latest
build:
context: ./server
dockerfile: Dockerfile
container_name: backend
ports:
- "3000:3000"

View file

@ -68,7 +68,6 @@ app.use(session({
}));
authManager = new AuthManager(app)
app.use(errorHandler)
// Start server

View file

@ -1,8 +1,19 @@
var OAuth2Strategy = require('passport-oauth2')
var authUserAssoc = require('../../../models/authUserAssociation')
var users = require('../../../models/users')
var { hasNestedValue } = require('../../../utils')
var jwt = require('../../../middleware/jwtToken')
class PassportOAuth {
constructor(passportjs,auth_name){
this.passportjs = passportjs
this.auth_name = auth_name
}
register(app, passport,endpoint, name, provider) {
const cb_url =`${process.env['BACKEND_URL']}${endpoint}/${name}/callback`
const self = this
passport.use(name, new OAuth2Strategy({
authorizationURL: provider.OAUTH_AUTHORIZATION_URL,
tokenURL: provider.OAUTH_TOKEN_URL,
@ -18,16 +29,33 @@ class PassportOAuth {
});
const userInfo = await userInfoResponse.json();
const user = {
id: userInfo.sub,
let received_user = {
auth_id: userInfo.sub,
email: userInfo.email,
name: userInfo.name,
groups: userInfo.groups ?? [],
accessToken: accessToken,
refreshToken: refreshToken,
expiresIn: params.expires_in
roles: []
};
if(hasNestedValue(userInfo,provider.OAUTH_ROLE_TEACHER_VALUE)) received_user.roles.push('teacher')
if(hasNestedValue(userInfo,provider.OAUTH_ROLE_STUDENT_VALUE)) received_user.roles.push('student')
const user_association = await authUserAssoc.find_user_association(self.auth_name._id,received_user.auth_id)
let user_account = null
if(user_association){
user_account = await users.getById(user_association.user_id)
}
else {
let user_id = await users.getId(received_user.email)
user_account = user_id ? await users.getById(user_id) : await users.register(received_user.email,"")
await authUserAssoc.link(self.auth_name,received_user.auth_id,user_account._id)
}
user_account.name = received_user.name
user_account.roles = received_user.roles
await users.editUser(user_account)
self.passportjs.authenticate(user_account)
// Store the tokens in the session
req.session.oauth2Tokens = {
accessToken: accessToken,
@ -35,7 +63,7 @@ class PassportOAuth {
expiresIn: params.expires_in
};
return done(null, user);
return done(null, user_account);
} catch (error) {
console.error(`Erreur dans la strategie OAuth2 '${name}' : ${error}`);
return done(error);
@ -55,7 +83,14 @@ class PassportOAuth {
},
(req, res) => {
if (req.user) {
res.json(req.user)
// res.json(req.user)
//const redirectUrl = `http://your-frontend-url.com/oauth/callback?user=${encodeURIComponent(req.user)}`;
//res.redirect(redirectUrl);
const tokenToSave = jwt.create(req.user.email, req.user._id);
res.redirect('/oauth/callback?user=' + tokenToSave);
console.info(`L'utilisateur '${req.user.name}' vient de se connecter`)
} else {
res.status(401).json({ error: "L'authentification a échoué" });

View file

@ -1,6 +1,14 @@
var OpenIDConnectStrategy = require('passport-openidconnect')
var authUserAssoc = require('../../../models/authUserAssociation')
var users = require('../../../models/users')
var { hasNestedValue } = require('../../../utils')
var jwt = require('../../../middleware/jwtToken')
class PassportOpenIDConnect {
constructor(passportjs,auth_name){
this.passportjs = passportjs
this.auth_name = auth_name
}
async getConfigFromConfigURL(name,provider){
try{
@ -15,6 +23,7 @@ class PassportOpenIDConnect {
const config = await this.getConfigFromConfigURL(name,provider)
const cb_url =`${process.env['BACKEND_URL']}${endpoint}/${name}/callback`
const self = this
passport.use(name, new OpenIDConnectStrategy({
issuer: config.issuer,
@ -30,15 +39,35 @@ class PassportOpenIDConnect {
// patch pour la librairie permet d'obtenir les groupes, PR en cours mais "morte" : https://github.com/jaredhanson/passport-openidconnect/pull/101
async function(req, issuer, profile, times, tok, done) {
try {
const user = {
id: profile.id,
const received_user = {
auth_id: profile.id,
email: profile.emails[0].value,
name: profile.name.givenName,
groups: profile.groups[0].value ?? []
roles: []
};
return done(null, user);
} catch (error) {
if(hasNestedValue(profile,provider.OIDC_ROLE_TEACHER_VALUE)) received_user.roles.push('teacher')
if(hasNestedValue(profile,provider.OIDC_ROLE_STUDENT_VALUE)) received_user.roles.push('student')
const user_association = await authUserAssoc.find_user_association(self.auth_name._id,received_user.auth_id)
let user_account = null
if(user_association){
user_account = await users.getById(user_association.user_id)
}
else {
let user_id = await users.getId(received_user.email)
user_account = user_id ? await users.getById(user_id) : await users.register(received_user.email,"")
await authUserAssoc.link(self.auth_name,received_user.auth_id,user_account._id)
}
user_account.name = received_user.name
user_account.roles = received_user.roles
await users.editUser(user_account)
self.passportjs.authenticate(user_account)
return done(null, user_account);
} catch (error) {
}
}));
@ -55,7 +84,11 @@ class PassportOpenIDConnect {
},
(req, res) => {
if (req.user) {
res.json(req.user)
// res.json(req.user)
const tokenToSave = jwt.create(req.user.email, req.user._id);
res.redirect('/oauth/callback?user=' + tokenToSave);
console.info(`L'utilisateur '${req.user.name}' vient de se connecter`)
} else {
res.status(401).json({ error: "L'authentification a échoué" });

View file

@ -1,5 +1,6 @@
const fs = require('fs');
var passport = require('passport')
var authprovider = require('../../models/authProvider')
class PassportJs{
constructor(authmanager,settings){
@ -9,17 +10,20 @@ class PassportJs{
this.endpoint = "/api/auth"
}
registerAuth(expressapp){
async registerAuth(expressapp){
expressapp.use(passport.initialize());
expressapp.use(passport.session());
for(const p of this.providers){
for(const [name,provider] of Object.entries(p)){
const auth_id = `passportjs_${provider.type}_${name}`
if(!(provider.type in this.registeredProviders)){
this.registerProvider(provider.type)
this.registerProvider(provider.type,auth_id)
}
try{
this.registeredProviders[provider.type].register(expressapp,passport,this.endpoint,name,provider)
authprovider.create(auth_id)
} catch(error){
console.error(`La connexion ${name} de type ${provider.type} n'as pu être chargé.`)
}
@ -35,17 +39,26 @@ class PassportJs{
});
}
registerProvider(providerType){
async registerProvider(providerType,auth_id){
try{
const providerPath = `${process.cwd()}/auth/modules/passport-providers/${providerType}.js`
const Provider = require(providerPath);
this.registeredProviders[providerType]= new Provider()
this.registeredProviders[providerType]= new Provider(this,auth_id)
console.info(`Le type de connexion '${providerType}' a été ajouté dans passportjs.`)
} catch(error){
console.error(`Le type de connexion '${providerType}' n'as pas pu être chargé dans passportjs.`)
}
}
register(userinfos){
return this.authmanager.register(userinfos)
}
authenticate(userinfos){
return this.authmanager.login(userinfos)
}
}
module.exports = PassportJs;

View file

@ -0,0 +1,44 @@
const db = require('../config/db.js')
const { ObjectId } = require('mongodb');
class AuthProvider {
constructor(name) {
this._id = new ObjectId();
this.name = name;
}
async getId(name){
await db.connect()
const conn = db.getConnection();
const collection = conn.collection('authprovider');
const existingauth = await collection.findOne({ name:name });
if(existingauth){
return existingauth._id
}
return null
}
async create(name) {
await db.connect()
const conn = db.getConnection();
const collection = conn.collection('authprovider');
const existingauth = await collection.findOne({ name:name });
if(existingauth){
return existingauth._id;
}
const newProvider = {
name:name
}
const result = await collection.insertOne(newProvider);
return result.insertedId;
}
}
module.exports = new AuthProvider;

View file

@ -0,0 +1,59 @@
const authProvider = require('./authProvider.js')
const db = require('../config/db.js')
const { ObjectId } = require('mongodb');
class AuthUserAssociation {
constructor(authProviderId, authId, userId) {
this._id = new ObjectId();
this.authProvider_id = authProviderId;
this.auth_id = authId;
this.user_id = userId;
this.connected = false;
}
async find_user_association(provider_name,auth_id){
await db.connect()
const conn = db.getConnection();
const collection = conn.collection('authUserAssociation');
const provider_id = await authProvider.getId(provider_name)
const userAssociation = await collection.findOne({ authProvider_id: provider_id,auth_id,auth_id });
return userAssociation
}
async link(provider_name,auth_id,user_id){
await db.connect()
const conn = db.getConnection();
const collection = conn.collection('authUserAssociation');
const provider_id = await authProvider.getId(provider_name)
const userAssociation = await collection.findOne({ authProvider_id: provider_id, user_id: user_id });
if(!userAssociation){
return await collection.insertOne({
_id:ObjectId,
authProvider_id:provider_id,
auth_id:auth_id,
user_id:user_id,
})
}
}
async unlink(provider_name,user_id){
await db.connect()
const conn = db.getConnection();
const collection = conn.collection('authUserAssociation');
const provider_id = await authProvider.getId(provider_name)
const userAssociation = await collection.findOne({ authProvider_id: provider_id, user_id: user_id });
if(userAssociation){
return await collection.deleteOne(userAssociation)
} else return null
}
}
module.exports = new AuthUserAssociation;

View file

@ -0,0 +1,13 @@
const db = require('../config/db.js')
const { ObjectId } = require('mongodb');
class AuthUserAssoc {
constructor(authProviderId, authId, userId) {
this._id = new ObjectId();
this.authProvider_id = authProviderId;
this.auth_id = authId;
this.user_id = userId;
}
}

View file

@ -1,14 +1,13 @@
//user
const db = require('../config/db.js');
const bcrypt = require('bcrypt');
const AppError = require('../middleware/AppError.js');
const { USER_ALREADY_EXISTS } = require('../constants/errorCodes');
const Folders = require('../models/folders.js');
const db = require("../config/db.js");
const bcrypt = require("bcrypt");
const AppError = require("../middleware/AppError.js");
const { USER_ALREADY_EXISTS } = require("../constants/errorCodes");
const Folders = require("../models/folders.js");
class Users {
async hashPassword(password) {
return await bcrypt.hash(password, 10)
return await bcrypt.hash(password, 10);
}
generatePassword() {
@ -16,14 +15,14 @@ class Users {
}
async verify(password, hash) {
return await bcrypt.compare(password, hash)
return await bcrypt.compare(password, hash);
}
async register(email, password) {
await db.connect()
await db.connect();
const conn = db.getConnection();
const userCollection = conn.collection('users');
const userCollection = conn.collection("users");
const existingUser = await userCollection.findOne({ email: email });
@ -34,23 +33,39 @@ class Users {
const newUser = {
email: email,
password: await this.hashPassword(password),
created_at: new Date()
created_at: new Date(),
};
await userCollection.insertOne(newUser);
let created_user = await userCollection.insertOne(newUser);
let user = await this.getById(created_user.insertedId)
const folderTitle = 'Dossier par Défaut';
const folderTitle = "Dossier par Défaut";
const userId = newUser._id.toString();
await Folders.create(folderTitle, userId);
// TODO: verif if inserted properly...
return user;
}
async login(userid) {
await db.connect();
const conn = db.getConnection();
const userCollection = conn.collection("users");
const user = await userCollection.findOne({ _id: userid });
if (!user) {
return false;
}
return user;
}
async login(email, password) {
await db.connect()
await db.connect();
const conn = db.getConnection();
const userCollection = conn.collection('users');
const userCollection = conn.collection("users");
const user = await userCollection.findOne({ email: email });
@ -74,25 +89,28 @@ class Users {
}
async changePassword(email, newPassword) {
await db.connect()
await db.connect();
const conn = db.getConnection();
const userCollection = conn.collection('users');
const userCollection = conn.collection("users");
const hashedPassword = await this.hashPassword(newPassword);
const result = await userCollection.updateOne({ email }, { $set: { password: hashedPassword } });
const result = await userCollection.updateOne(
{ email },
{ $set: { password: hashedPassword } }
);
if (result.modifiedCount != 1) return null;
return newPassword
return newPassword;
}
async delete(email) {
await db.connect()
await db.connect();
const conn = db.getConnection();
const userCollection = conn.collection('users');
const userCollection = conn.collection("users");
const result = await userCollection.deleteOne({ email });
@ -102,10 +120,10 @@ class Users {
}
async getId(email) {
await db.connect()
await db.connect();
const conn = db.getConnection();
const userCollection = conn.collection('users');
const userCollection = conn.collection("users");
const user = await userCollection.findOne({ email: email });
@ -116,6 +134,47 @@ class Users {
return user._id;
}
async getById(id) {
await db.connect();
const conn = db.getConnection();
const userCollection = conn.collection("users");
const user = await userCollection.findOne({ _id: id });
if (!user) {
return false;
}
module.exports = new Users;
return user;
}
async editUser(userInfo) {
await db.connect();
const conn = db.getConnection();
const userCollection = conn.collection("users");
const user = await userCollection.findOne({ _id: userInfo.id });
if (!user) {
return false;
}
const updatedFields = { ...userInfo };
delete updatedFields.id;
const result = await userCollection.updateOne(
{ _id: userInfo.id },
{ $set: updatedFields }
);
if (result.modifiedCount === 1) {
return true;
}
return false;
}
}
module.exports = new Users();

35
server/utils.js Normal file
View file

@ -0,0 +1,35 @@
function hasNestedValue(obj, path, delimiter = "_") {
const keys = path.split(delimiter);
let current = obj;
for (const key of keys) {
while(Array.isArray(current) && current.length == 1 && current[0]){
current = current[0]
}
while(current['value']){
current = current.value
}
if (current && typeof current === "object") {
if (Array.isArray(current)) {
const index = current.findIndex(x => x == key)
if (index != -1) {
current = current[index];
} else {
return false;
}
} else if (key in current) {
current = current[key];
} else {
return false;
}
} else {
return false;
}
}
return true;
}
module.exports = { hasNestedValue};