Incorporate auth config

This commit is contained in:
Gabriel Matte 2024-09-28 17:08:11 -04:00
parent ec9adc1440
commit d97f3f55e2
10 changed files with 284 additions and 183 deletions

View file

@ -20,29 +20,10 @@
"OIDC_CLIENT_ID": "your_oidc_client_id",
"OIDC_CLIENT_SECRET": "your_oidc_client_secret",
"OIDC_ISSUER_URL": "https://your-issuer.com",
"OIDC_CALLBACK_URL": "http://localhost:3000/auth/oidc/callback"
}
},
{
"provider3": {
"type": "oauth",
"OAUTH_AUTHORIZATION_URL": "https://www.testurl.com/oauth2/authorize",
"OAUTH_TOKEN_URL": "https://www.testurl.com/oauth2/token",
"OAUTH_CLIENT_ID": "your_oauth_client_id",
"OAUTH_CLIENT_SECRET": "your_oauth_client_secret",
"OAUTH_CALLBACK_URL": "https://localhost:3000/auth/provider/callback",
"OAUTH_ADD_SCOPE": "scopes",
"OAUTH_ROLE_TEACHER_VALUE": "teacher-claim-value",
"OAUTH_ROLE_STUDENT_VALUE": "student-claim-value"
}
},
{
"provider4": {
"type": "oidc",
"OIDC_CLIENT_ID": "your_oidc_client_id",
"OIDC_CLIENT_SECRET": "your_oidc_client_secret",
"OIDC_ISSUER_URL": "https://your-issuer.com",
"OIDC_CALLBACK_URL": "http://localhost:3000/auth/oidc/callback"
"OIDC_CALLBACK_URL": "http://localhost:3000/auth/oidc/callback",
"OIDC_ADD_SCOPE": "scopes",
"OIDC_ROLE_TEACHER_VALUE": "teacher-claim-value",
"OIDC_ROLE_STUDENT_VALUE": "student-claim-value"
}
}
],

1
server/.gitignore vendored Normal file
View file

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

View file

@ -1,103 +1,193 @@
const request = require('supertest');
const AuthConfig = require('../config/auth.js');
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_CLIENT_ID": "your_oauth_client_id",
"OAUTH_CLIENT_SECRET": "your_oauth_client_secret",
"OAUTH_CALLBACK_URL": "https://localhost:3000/auth/provider/callback",
"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_CALLBACK_URL": "http://localhost:3000/auth/oidc/callback"
}
}
],
"simple-login": {
"enabled": true,
"name": "provider3",
"SESSION_SECRET": "your_session_secret"
}
}
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', () => {
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
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 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 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 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 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
}
}
]
}
};
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);
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 : [
// 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_CALLBACK_URL"
"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",
},
},
],
},
};
authConfigInstance.loadConfigTest(validModule); // On injecte la configuration mockée
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
expect(logSpy).toHaveBeenCalledTimes(0);
});
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);
});
/*
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);
});
*/
})
);

View file

@ -62,8 +62,6 @@ app.use(session({
}));
authManager = new AuthManager(app)
authManager.addModule('passport-js')
authManager.registerAuths()
app.use(errorHandler)

View file

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

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')
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é" });
}
}
);

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

View file

@ -2,7 +2,7 @@ const fs = require('fs');
const path = require('path');
const pathAuthConfig = './auth_config.json';
const configPath = path.join(__dirname, pathAuthConfig);
const configPath = path.join(process.cwd(), pathAuthConfig);
class AuthConfig {
@ -14,6 +14,7 @@ class AuthConfig {
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;
@ -89,11 +90,11 @@ class AuthConfig {
// Méthode pour vérifier si tous les providers ont les variables nécessaires
validateProvidersConfig() {
const requiredOAuthFields = [
'OAUTH_AUTHORIZATION_URL', 'OAUTH_TOKEN_URL', 'OAUTH_CLIENT_ID', 'OAUTH_CLIENT_SECRET', 'OAUTH_CALLBACK_URL'
'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_CALLBACK_URL'
'OIDC_CLIENT_ID', 'OIDC_CLIENT_SECRET', 'OIDC_ISSUER_URL', 'OIDC_ROLE_TEACHER_VALUE', 'OIDC_ROLE_STUDENT_VALUE'
];
const missingFieldsReport = [];