diff --git a/docker-compose.yaml b/docker-compose.yaml index 284a46e..947fb2a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -23,6 +23,8 @@ services: EMAIL_PSW: 'vvml wmfr dkzb vjzb' JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe FRONTEND_URL: "http://localhost:5173" + volumes: + - ./server/auth_config.json:/usr/src/app/serveur/config/auth_config.json depends_on: - mongo restart: always diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..47c9c3b --- /dev/null +++ b/server/.gitignore @@ -0,0 +1 @@ +auth_config.json \ No newline at end of file diff --git a/server/__tests__/auth.test.js b/server/__tests__/auth.test.js new file mode 100644 index 0000000..f3f288c --- /dev/null +++ b/server/__tests__/auth.test.js @@ -0,0 +1,206 @@ +const request = require("supertest"); +const AuthConfig = require("../config/auth.js"); +const AuthManager = require("../auth/auth-manager.js"); + +const mockConfig = { + auth: { + passportjs: [ + { + provider1: { + type: "oauth", + OAUTH_AUTHORIZATION_URL: "https://www.testurl.com/oauth2/authorize", + OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token", + OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/", + OAUTH_CLIENT_ID: "your_oauth_client_id", + OAUTH_CLIENT_SECRET: "your_oauth_client_secret", + OAUTH_ADD_SCOPE: "scopes", + OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value", + OAUTH_ROLE_STUDENT_VALUE: "student-claim-value", + }, + }, + { + provider2: { + type: "oidc", + OIDC_CLIENT_ID: "your_oidc_client_id", + OIDC_CLIENT_SECRET: "your_oidc_client_secret", + OIDC_ISSUER_URL: "https://your-issuer.com", + OIDC_ROLE_TEACHER_VALUE: "teacher-claim-value", + OIDC_ROLE_STUDENT_VALUE: "student-claim-value", + }, + }, + ], + "simple-login": { + enabled: true, + name: "provider3", + SESSION_SECRET: "your_session_secret", + }, + }, +}; + +// Créez une instance de AuthConfig en utilisant la configuration mockée +describe( + "AuthConfig Class Tests", + () => { + let authConfigInstance; + + // Initialisez l'instance avec la configuration mockée + beforeAll(() => { + authConfigInstance = new AuthConfig(); + authConfigInstance.loadConfigTest(mockConfig); // On injecte la configuration mockée + }); + + it("devrait retourner la configuration PassportJS", () => { + const config = authConfigInstance.getPassportJSConfig(); + expect(config).toHaveProperty("provider1"); + expect(config).toHaveProperty("provider2"); + }); + + it("devrait retourner la configuration Simple Login", () => { + const config = authConfigInstance.getSimpleLoginConfig(); + expect(config).toHaveProperty("name", "provider3"); + expect(config).toHaveProperty("SESSION_SECRET", "your_session_secret"); + }); + + it("devrait retourner les providers OAuth", () => { + const oauthProviders = authConfigInstance.getOAuthProviders(); + expect(Array.isArray(oauthProviders)).toBe(true); + expect(oauthProviders.length).toBe(1); // Il y a un seul provider OAuth + expect(oauthProviders[0]).toHaveProperty("provider1"); + }); + + it("devrait valider la configuration des providers", () => { + expect(() => authConfigInstance.validateProvidersConfig()).not.toThrow(); + }); + + it("devrait lever une erreur si une configuration manque", () => { + const invalidMockConfig = { + auth: { + passportjs: [ + { + provider1: { + type: "oauth", + OAUTH_CLIENT_ID: "your_oauth_client_id", // Il manque des champs nécessaires + }, + }, + ], + }, + }; + + const instanceWithInvalidConfig = new AuthConfig(); + instanceWithInvalidConfig.loadConfigTest(invalidMockConfig); + + // Vérifiez que l'erreur est lancée avec les champs manquants corrects + expect(() => instanceWithInvalidConfig.validateProvidersConfig()).toThrow( + new Error(`Configuration invalide pour les providers suivants : [ + { + "provider": "provider1", + "missingFields": [ + "OAUTH_AUTHORIZATION_URL", + "OAUTH_TOKEN_URL", + "OAUTH_USERINFO_URL", + "OAUTH_CLIENT_SECRET", + "OAUTH_ROLE_TEACHER_VALUE", + "OAUTH_ROLE_STUDENT_VALUE" + ] + } +]`) + ); + }); + }, + + describe("Auth Module Registration", () => { + let expressMock = jest.mock("express"); + expressMock.use = () => {} + expressMock.get = () => {} + + let authConfigInstance; + let logSpy; + + // Initialisez l'instance avec la configuration mockée + beforeAll(() => { + authConfigInstance = new AuthConfig(); + }); + + it("should load valid modules", () => { + const logSpy = jest.spyOn(global.console, "error"); + const validModule = { + auth: { + passportjs: [ + { + provider1: { + type: "oauth", + OAUTH_AUTHORIZATION_URL: + "https://www.testurl.com/oauth2/authorize", + OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token", + OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/", + OAUTH_CLIENT_ID: "your_oauth_client_id", + OAUTH_CLIENT_SECRET: "your_oauth_client_secret", + OAUTH_ADD_SCOPE: "scopes", + OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value", + OAUTH_ROLE_STUDENT_VALUE: "student-claim-value", + }, + provider2: { + type: "oauth", + OAUTH_AUTHORIZATION_URL: + "https://www.testurl.com/oauth2/authorize", + OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token", + OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/", + OAUTH_CLIENT_ID: "your_oauth_client_id", + OAUTH_CLIENT_SECRET: "your_oauth_client_secret", + OAUTH_ADD_SCOPE: "scopes", + OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value", + OAUTH_ROLE_STUDENT_VALUE: "student-claim-value", + }, + }, + ], + }, + }; + authConfigInstance.loadConfigTest(validModule); // On injecte la configuration mockée + authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config); + expect(logSpy).toHaveBeenCalledTimes(0); + logSpy.mockClear(); + }); + + it("should not load invalid modules", () => { + const logSpy = jest.spyOn(global.console, "error"); + const invalidModule = { + auth: { + ModuleX:{} + }, + }; + authConfigInstance.loadConfigTest(invalidModule); // On injecte la configuration mockée + authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config); + expect(logSpy).toHaveBeenCalledTimes(1); + logSpy.mockClear(); + }); + + + it("should not load invalid provider from passport", () => { + const logSpy = jest.spyOn(global.console, "error"); + const validModuleInvalidProvider = { + auth: { + passportjs: [ + { + provider1: { + type: "x", + OAUTH_AUTHORIZATION_URL: + "https://www.testurl.com/oauth2/authorize", + OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token", + OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/", + OAUTH_CLIENT_ID: "your_oauth_client_id", + OAUTH_CLIENT_SECRET: "your_oauth_client_secret", + OAUTH_ADD_SCOPE: "scopes", + OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value", + OAUTH_ROLE_STUDENT_VALUE: "student-claim-value", + }, + }, + ], + }, + }; + authConfigInstance.loadConfigTest(validModuleInvalidProvider); // On injecte la configuration mockée + authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config); + expect(logSpy).toHaveBeenCalledTimes(2); + logSpy.mockClear(); + }); + }) +); diff --git a/server/app.js b/server/app.js index e1ab3a6..73d9c53 100644 --- a/server/app.js +++ b/server/app.js @@ -13,6 +13,7 @@ const folderRouter = require('./routers/folders.js'); const quizRouter = require('./routers/quiz.js'); const imagesRouter = require('./routers/images.js') const AuthManager = require('./auth/auth-manager.js') +const authRouter = require('./routers/auth.js') // Setup environement dotenv.config(); @@ -49,6 +50,7 @@ app.use('/api/user', userRouter); app.use('/api/folder', folderRouter); app.use('/api/quiz', quizRouter); app.use('/api/image', imagesRouter); +app.use('/api/auth', authRouter); // Add Auths methods const session = require('express-session'); @@ -60,8 +62,6 @@ app.use(session({ })); authManager = new AuthManager(app) -authManager.addModule('passport-js') -authManager.registerAuths() app.use(errorHandler) diff --git a/server/auth/auth-manager.js b/server/auth/auth-manager.js index 9c0ea6e..306c07f 100644 --- a/server/auth/auth-manager.js +++ b/server/auth/auth-manager.js @@ -1,26 +1,20 @@ const fs = require('fs'); - -const settings = { - "passport-js":{ - "gmatte" : { - type: "oauth", - authorization_url: process.env['OAUTH_AuthorizeUrl'], - client_id : process.env['OAUTH_ClientID'], - client_secret: process.env['OAUTH_ClientSecret'], - config_url: process.env['OAUTH_ConfigUrl'], - userinfo_url: process.env['OAUTH_UserinfoUrl'], - token_url: process.env['OAUTH_TokenUrl'], - logout_url: process.env['OAUTH_LogoutUrl'], - jwks : process.env['OAUTH_JWKS'], - scopes: ['openid','email','profile','groups','offline_access'] - }, - } -} +const AuthConfig = require('../config/auth.js'); class AuthManager{ - constructor(expressapp){ + constructor(expressapp,configs=null){ this.modules = [] this.app = expressapp + + this.configs = configs ?? (new AuthConfig()).loadConfig() + this.addModules() + this.registerAuths() + } + + async addModules(){ + for(const module in this.configs.auth){ + this.addModule(module) + } } async addModule(name){ @@ -28,25 +22,23 @@ class AuthManager{ if(fs.existsSync(modulePath)){ const Module = require(modulePath); - this.modules.push(new Module(this,settings[name])); - console.debug(`Auth module ${name} added`) + this.modules.push(new Module(this,this.configs.auth[name])); + console.info(`Module d'authentification '${name}' ajouté`) + } else{ + console.error(`Le module d'authentification ${name} n'as pas été chargé car il est introuvable`) } } async registerAuths(){ for(const module of this.modules){ - module.registerAuth(this.app) + try{ + module.registerAuth(this.app) + } catch(error){ + console.error(`L'enregistrement du module ${module} a échoué.`) + } } } - async showAuths(){ - let authsData = [] - for(const module in this.modules){ - authsData.push(module.showAuth()) - } - return authsData; - } - async login(userInfos){ // TODO global user login method console.log(userInfos) diff --git a/server/auth/modules/passport-js.js b/server/auth/modules/passport-js.js deleted file mode 100644 index dd336b8..0000000 --- a/server/auth/modules/passport-js.js +++ /dev/null @@ -1,42 +0,0 @@ -const fs = require('fs'); -var passport = require('passport') - -class PassportJs{ - constructor(authmanager,settings){ - this.authmanager = authmanager - this.registeredProviders = {} - this.providers = Object.entries(settings) - } - - registerAuth(expressapp){ - expressapp.use(passport.initialize()); - expressapp.use(passport.session()); - - for(const [name,provider] of this.providers){ - if(!(provider.type in this.registeredProviders)){ - this.registerProvider(provider.type) - } - this.registeredProviders[provider.type].register(expressapp,passport,name,provider) - } - - passport.serializeUser(function(user, done) { - done(null, user); - }); - - passport.deserializeUser(function(user, done) { - done(null, user); - }); - } - - registerProvider(providerType){ - const providerPath = `${process.cwd()}/auth/modules/passport-providers/${providerType}.js` - - if(fs.existsSync(providerPath)){ - const Provider = require(providerPath); - this.registeredProviders[providerType]= new Provider() - } - } - -} - -module.exports = PassportJs; \ No newline at end of file diff --git a/server/auth/modules/passport-providers/oauth.js b/server/auth/modules/passport-providers/oauth.js index 19c7938..8391a0c 100644 --- a/server/auth/modules/passport-providers/oauth.js +++ b/server/auth/modules/passport-providers/oauth.js @@ -1,18 +1,18 @@ var OAuth2Strategy = require('passport-oauth2') class PassportOAuth { - register(app, passport, name, provider) { + register(app, passport,endpoint, name, provider) { passport.use(name, new OAuth2Strategy({ - authorizationURL: provider.authorization_url, - tokenURL: provider.token_url, - clientID: provider.client_id, - clientSecret: provider.client_secret, - callbackURL: `http://localhost:4400/api/auth/gmatte/callback`, + authorizationURL: provider.OAUTH_AUTHORIZATION_URL, + tokenURL: provider.OAUTH_TOKEN_URL, + clientID: provider.OAUTH_CLIENT_ID, + clientSecret: provider.OAUTH_CLIENT_SECRET, + callbackURL: `${endpoint}/${name}/callback`, passReqToCallback: true }, async function(req, accessToken, refreshToken, params, profile, done) { try { - const userInfoResponse = await fetch(provider.userinfo_url, { + const userInfoResponse = await fetch(provider.OAUTH_USERINFO_URL, { headers: { 'Authorization': `Bearer ${accessToken}` } }); const userInfo = await userInfoResponse.json(); @@ -35,27 +35,28 @@ class PassportOAuth { return done(null, user); } catch (error) { - console.error(`Error in OAuth2 Strategy ${name} :`, error); + console.error(`Erreur dans la strategie OAuth2 '${name}' : ${error}`); return done(error); } })); - app.get(`/api/auth/${name}`, (req, res, next) => { + app.get(`${endpoint}/${name}`, (req, res, next) => { passport.authenticate(name, { - scope: provider.scopes.join(' ') ?? 'openid profile email offline_access', + scope: 'openid profile email offline_access'+ ` ${provider.OAUTH_ADD_SCOPE}`, prompt: 'consent' })(req, res, next); }); - app.get(`/api/auth/${name}/callback`, + app.get(`${endpoint}/${name}/callback`, (req, res, next) => { passport.authenticate(name, { failureRedirect: '/login' })(req, res, next); }, (req, res) => { if (req.user) { res.json(req.user) + console.info(`L'utilisateur '${req.user.name}' vient de se connecter`) } else { - res.status(401).json({ error: 'Authentication failed' }); + res.status(401).json({ error: "L'authentification a échoué" }); } } ); diff --git a/server/auth/modules/passportjs.js b/server/auth/modules/passportjs.js new file mode 100644 index 0000000..e65b53c --- /dev/null +++ b/server/auth/modules/passportjs.js @@ -0,0 +1,51 @@ +const fs = require('fs'); +var passport = require('passport') + +class PassportJs{ + constructor(authmanager,settings){ + this.authmanager = authmanager + this.registeredProviders = {} + this.providers = settings + this.endpoint = "/api/auth" + } + + registerAuth(expressapp){ + expressapp.use(passport.initialize()); + expressapp.use(passport.session()); + + for(const p of this.providers){ + for(const [name,provider] of Object.entries(p)){ + if(!(provider.type in this.registeredProviders)){ + this.registerProvider(provider.type) + } + try{ + this.registeredProviders[provider.type].register(expressapp,passport,this.endpoint,name,provider) + } catch(error){ + console.error(`La connexion ${name} de type ${provider.type} n'as pu être chargé.`) + } + } + } + + passport.serializeUser(function(user, done) { + done(null, user); + }); + + passport.deserializeUser(function(user, done) { + done(null, user); + }); + } + + registerProvider(providerType){ + try{ + const providerPath = `${process.cwd()}/auth/modules/passport-providers/${providerType}.js` + const Provider = require(providerPath); + this.registeredProviders[providerType]= new Provider() + 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.`) + } + } + +} + +module.exports = PassportJs; \ No newline at end of file diff --git a/server/auth_config.json.example b/server/auth_config.json.example new file mode 100644 index 0000000..c2aa256 --- /dev/null +++ b/server/auth_config.json.example @@ -0,0 +1,28 @@ +{ + "auth": { + "passportjs": + [ + { + "gmatte": { + "type": "oauth", + "OAUTH_AUTHORIZATION_URL": "https://auth.gmatte.xyz/application/o/authorize/", + "OAUTH_TOKEN_URL": "https://auth.gmatte.xyz/application/o/token/", + "OAUTH_USERINFO_URL": "https://auth.gmatte.xyz/application/o/userinfo/", + "OAUTH_CLIENT_ID": "clientID", + "OAUTH_CLIENT_SECRET": "clientSecret", + "OAUTH_ADD_SCOPE": "groups", + "OAUTH_ROLE_TEACHER_VALUE": "groups_evaluetonsavoir-prof", + "OAUTH_ROLE_STUDENT_VALUE": "groups_evaluetonsavoir" + } + }, + { + "oidc":{ + "type":"oidc" + } + } + ], + "Module X":{ + + } + } +} \ No newline at end of file diff --git a/server/config/auth.js b/server/config/auth.js new file mode 100644 index 0000000..6d2b425 --- /dev/null +++ b/server/config/auth.js @@ -0,0 +1,185 @@ +const fs = require('fs'); +const path = require('path'); +const pathAuthConfig = './auth_config.json'; + +const configPath = path.join(process.cwd(), pathAuthConfig); + +class AuthConfig { + + config = null; + + + // Méthode pour lire le fichier de configuration JSON + loadConfig() { + try { + const configData = fs.readFileSync(configPath, 'utf-8'); + this.config = JSON.parse(configData); + return this.config + } catch (error) { + console.error("Erreur lors de la lecture du fichier de configuration :", error); + return null; + } + } + + // Méthode pour load le fichier de test + loadConfigTest(mockConfig) { + this.config = mockConfig; + } + + // Méthode pour retourner la configuration des fournisseurs PassportJS + getPassportJSConfig() { + if (this.config && this.config.auth && this.config.auth.passportjs) { + const passportConfig = {}; + + this.config.auth.passportjs.forEach(provider => { + const providerName = Object.keys(provider)[0]; + passportConfig[providerName] = provider[providerName]; + }); + + return passportConfig; + } else { + return { error: "Aucune configuration PassportJS disponible." }; + } + } + + // Méthode pour retourner la configuration de Simple Login + getSimpleLoginConfig() { + if (this.config && this.config.auth && this.config.auth["simple-login"]) { + return this.config.auth["simple-login"]; + } else { + return { error: "Aucune configuration Simple Login disponible." }; + } + } + + // Méthode pour retourner tous les providers de type OAuth + getOAuthProviders() { + if (this.config && this.config.auth && this.config.auth.passportjs) { + const oauthProviders = this.config.auth.passportjs.filter(provider => { + const providerName = Object.keys(provider)[0]; + return provider[providerName].type === 'oauth'; + }); + + if (oauthProviders.length > 0) { + return oauthProviders; + } else { + return { error: "Aucun fournisseur OAuth disponible." }; + } + } else { + return { error: "Aucune configuration PassportJS disponible." }; + } + } + + // Méthode pour retourner tous les providers de type OIDC + getOIDCProviders() { + if (this.config && this.config.auth && this.config.auth.passportjs) { + const oidcProviders = this.config.auth.passportjs.filter(provider => { + const providerName = Object.keys(provider)[0]; + return provider[providerName].type === 'oidc'; + }); + + if (oidcProviders.length > 0) { + return oidcProviders; + } else { + return { error: "Aucun fournisseur OIDC disponible." }; + } + } else { + return { error: "Aucune configuration PassportJS disponible." }; + } + } + + // Méthode pour vérifier si tous les providers ont les variables nécessaires + validateProvidersConfig() { + const requiredOAuthFields = [ + 'OAUTH_AUTHORIZATION_URL', 'OAUTH_TOKEN_URL','OAUTH_USERINFO_URL', 'OAUTH_CLIENT_ID', 'OAUTH_CLIENT_SECRET', 'OAUTH_ROLE_TEACHER_VALUE', 'OAUTH_ROLE_STUDENT_VALUE' + ]; + + const requiredOIDCFields = [ + 'OIDC_CLIENT_ID', 'OIDC_CLIENT_SECRET', 'OIDC_ISSUER_URL', 'OIDC_ROLE_TEACHER_VALUE', 'OIDC_ROLE_STUDENT_VALUE' + ]; + + const missingFieldsReport = []; + + if (this.config && this.config.auth && this.config.auth.passportjs) { + this.config.auth.passportjs.forEach(provider => { + const providerName = Object.keys(provider)[0]; + const providerConfig = provider[providerName]; + + let missingFields = []; + + // Vérification des providers de type OAuth + if (providerConfig.type === 'oauth') { + missingFields = requiredOAuthFields.filter(field => !(field in providerConfig)); + } + // Vérification des providers de type OIDC + else if (providerConfig.type === 'oidc') { + missingFields = requiredOIDCFields.filter(field => !(field in providerConfig)); + } + + // Si des champs manquent, on les ajoute au rapport + if (missingFields.length > 0) { + missingFieldsReport.push({ + provider: providerName, + missingFields: missingFields + }); + } + }); + + // Si des champs manquent, lever une exception + if (missingFieldsReport.length > 0) { + throw new Error(`Configuration invalide pour les providers suivants : ${JSON.stringify(missingFieldsReport, null, 2)}`); + } else { + console.log("Configuration auth_config.json: Tous les providers ont les variables nécessaires.") + return { success: "Tous les providers ont les variables nécessaires." }; + } + } else { + throw new Error("Aucune configuration PassportJS disponible."); + } + } + + // Méthode pour retourner la configuration des fournisseurs PassportJS pour le frontend + getActiveAuth() { + if (this.config && this.config.auth) { + const passportConfig = {}; + + // Gestion des providers PassportJS + if (this.config.auth.passportjs) { + this.config.auth.passportjs.forEach(provider => { + const providerName = Object.keys(provider)[0]; + const providerConfig = provider[providerName]; + + passportConfig[providerName] = {}; + + if (providerConfig.type === 'oauth') { + passportConfig[providerName] = { + type: providerConfig.type, + authorizationUrl: providerConfig.OAUTH_AUTHORIZATION_URL, + callbackUrl: providerConfig.OAUTH_CALLBACK_URL, + }; + } else if (providerConfig.type === 'oidc') { + passportConfig[providerName] = { + type: providerConfig.type, + issuerUrl: providerConfig.OIDC_ISSUER_URL, + callbackUrl: providerConfig.OIDC_CALLBACK_URL + }; + } + }); + } + + // Gestion du Simple Login + if (this.config.auth["simple-login"] && this.config.auth["simple-login"].enabled) { + passportConfig['simple-login'] = { + type: "simple-login", + name: this.config.auth["simple-login"].name + }; + } + + return passportConfig; + } else { + return { error: "Aucune configuration d'authentification disponible." }; + } + } + + +} + +module.exports = AuthConfig; diff --git a/server/controllers/auth.js b/server/controllers/auth.js new file mode 100644 index 0000000..21fa3b1 --- /dev/null +++ b/server/controllers/auth.js @@ -0,0 +1,25 @@ +const AuthConfig = require('../config/auth.js'); + +class authController { + + async getActive(req, res, next) { + try { + + const authC = new AuthConfig(); + authC.loadConfig(); + + const authActive = authC.getActiveAuth(); + + const response = { + authActive + }; + return res.json(response); + } + catch (error) { + return next(error); // Gérer l'erreur + } + } + +} + +module.exports = new authController; \ No newline at end of file diff --git a/server/routers/auth.js b/server/routers/auth.js new file mode 100644 index 0000000..8e8fb4a --- /dev/null +++ b/server/routers/auth.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const jwt = require('../middleware/jwtToken.js'); + +const authController = require('../controllers/auth.js') + +router.get("/getActiveAuth",jwt.authenticate, authController.getActive); + +module.exports = router; \ No newline at end of file