Merge pull request #140 from ets-cfuhrman-pfe/adds-configuration-to-oauth-integration

Adds configuration to oauth integration
This commit is contained in:
roesnerb 2024-09-28 18:17:49 -04:00 committed by GitHub
commit ac8b6d34b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 543 additions and 85 deletions

View file

@ -23,6 +23,8 @@ services:
EMAIL_PSW: 'vvml wmfr dkzb vjzb' EMAIL_PSW: 'vvml wmfr dkzb vjzb'
JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe
FRONTEND_URL: "http://localhost:5173" FRONTEND_URL: "http://localhost:5173"
volumes:
- ./server/auth_config.json:/usr/src/app/serveur/config/auth_config.json
depends_on: depends_on:
- mongo - mongo
restart: always restart: always

1
server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
auth_config.json

View file

@ -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();
});
})
);

View file

@ -13,6 +13,7 @@ const folderRouter = require('./routers/folders.js');
const quizRouter = require('./routers/quiz.js'); const quizRouter = require('./routers/quiz.js');
const imagesRouter = require('./routers/images.js') const imagesRouter = require('./routers/images.js')
const AuthManager = require('./auth/auth-manager.js') const AuthManager = require('./auth/auth-manager.js')
const authRouter = require('./routers/auth.js')
// Setup environement // Setup environement
dotenv.config(); dotenv.config();
@ -49,6 +50,7 @@ app.use('/api/user', userRouter);
app.use('/api/folder', folderRouter); app.use('/api/folder', folderRouter);
app.use('/api/quiz', quizRouter); app.use('/api/quiz', quizRouter);
app.use('/api/image', imagesRouter); app.use('/api/image', imagesRouter);
app.use('/api/auth', authRouter);
// Add Auths methods // Add Auths methods
const session = require('express-session'); const session = require('express-session');
@ -60,8 +62,6 @@ app.use(session({
})); }));
authManager = new AuthManager(app) authManager = new AuthManager(app)
authManager.addModule('passport-js')
authManager.registerAuths()
app.use(errorHandler) app.use(errorHandler)

View file

@ -1,26 +1,20 @@
const fs = require('fs'); const fs = require('fs');
const AuthConfig = require('../config/auth.js');
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']
},
}
}
class AuthManager{ class AuthManager{
constructor(expressapp){ constructor(expressapp,configs=null){
this.modules = [] this.modules = []
this.app = expressapp 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){ async addModule(name){
@ -28,25 +22,23 @@ class AuthManager{
if(fs.existsSync(modulePath)){ if(fs.existsSync(modulePath)){
const Module = require(modulePath); const Module = require(modulePath);
this.modules.push(new Module(this,settings[name])); this.modules.push(new Module(this,this.configs.auth[name]));
console.debug(`Auth module ${name} added`) 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(){ async registerAuths(){
for(const module of this.modules){ 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){ async login(userInfos){
// TODO global user login method // TODO global user login method
console.log(userInfos) console.log(userInfos)

View file

@ -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;

View file

@ -1,18 +1,18 @@
var OAuth2Strategy = require('passport-oauth2') var OAuth2Strategy = require('passport-oauth2')
class PassportOAuth { class PassportOAuth {
register(app, passport, name, provider) { register(app, passport,endpoint, name, provider) {
passport.use(name, new OAuth2Strategy({ passport.use(name, new OAuth2Strategy({
authorizationURL: provider.authorization_url, authorizationURL: provider.OAUTH_AUTHORIZATION_URL,
tokenURL: provider.token_url, tokenURL: provider.OAUTH_TOKEN_URL,
clientID: provider.client_id, clientID: provider.OAUTH_CLIENT_ID,
clientSecret: provider.client_secret, clientSecret: provider.OAUTH_CLIENT_SECRET,
callbackURL: `http://localhost:4400/api/auth/gmatte/callback`, callbackURL: `${endpoint}/${name}/callback`,
passReqToCallback: true passReqToCallback: true
}, },
async function(req, accessToken, refreshToken, params, profile, done) { async function(req, accessToken, refreshToken, params, profile, done) {
try { try {
const userInfoResponse = await fetch(provider.userinfo_url, { const userInfoResponse = await fetch(provider.OAUTH_USERINFO_URL, {
headers: { 'Authorization': `Bearer ${accessToken}` } headers: { 'Authorization': `Bearer ${accessToken}` }
}); });
const userInfo = await userInfoResponse.json(); const userInfo = await userInfoResponse.json();
@ -35,27 +35,28 @@ class PassportOAuth {
return done(null, user); return done(null, user);
} catch (error) { } catch (error) {
console.error(`Error in OAuth2 Strategy ${name} :`, error); console.error(`Erreur dans la strategie OAuth2 '${name}' : ${error}`);
return done(error); return done(error);
} }
})); }));
app.get(`/api/auth/${name}`, (req, res, next) => { app.get(`${endpoint}/${name}`, (req, res, next) => {
passport.authenticate(name, { passport.authenticate(name, {
scope: provider.scopes.join(' ') ?? 'openid profile email offline_access', scope: 'openid profile email offline_access'+ ` ${provider.OAUTH_ADD_SCOPE}`,
prompt: 'consent' prompt: 'consent'
})(req, res, next); })(req, res, next);
}); });
app.get(`/api/auth/${name}/callback`, app.get(`${endpoint}/${name}/callback`,
(req, res, next) => { (req, res, next) => {
passport.authenticate(name, { failureRedirect: '/login' })(req, res, next); passport.authenticate(name, { failureRedirect: '/login' })(req, res, next);
}, },
(req, res) => { (req, res) => {
if (req.user) { if (req.user) {
res.json(req.user) res.json(req.user)
console.info(`L'utilisateur '${req.user.name}' vient de se connecter`)
} else { } else {
res.status(401).json({ error: 'Authentication failed' }); res.status(401).json({ error: "L'authentification a échoué" });
} }
} }
); );

View file

@ -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;

View file

@ -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":{
}
}
}

185
server/config/auth.js Normal file
View file

@ -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;

View file

@ -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;

9
server/routers/auth.js Normal file
View file

@ -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;