|
|
@@ -1,11 +1,134 @@
|
|
|
const express = require('express');
|
|
|
const bcrypt = require('bcryptjs');
|
|
|
const jwt = require('jsonwebtoken');
|
|
|
+const { Issuer } = require('openid-client');
|
|
|
const pool = require('../db');
|
|
|
const { JWT_SECRET } = require('../auth');
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
+// ─── OIDC client (lazy-initialized) ────────────────────────────────────────
|
|
|
+
|
|
|
+let oidcClient = null;
|
|
|
+
|
|
|
+async function getOidcClient() {
|
|
|
+ if (oidcClient) return oidcClient;
|
|
|
+ const issuerUrl = process.env.OIDC_ISSUER;
|
|
|
+ if (!issuerUrl) return null;
|
|
|
+ try {
|
|
|
+ const issuer = await Issuer.discover(issuerUrl);
|
|
|
+ oidcClient = new issuer.Client({
|
|
|
+ client_id: process.env.OIDC_CLIENT_ID,
|
|
|
+ client_secret: process.env.OIDC_CLIENT_SECRET,
|
|
|
+ redirect_uris: [process.env.OIDC_REDIRECT_URI || 'http://localhost:3001/api/auth/oidc/callback'],
|
|
|
+ response_types: ['code'],
|
|
|
+ });
|
|
|
+ return oidcClient;
|
|
|
+ } catch (e) {
|
|
|
+ console.error('OIDC discovery failed:', e.message);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// State store simple en mémoire (suffisant pour usage mono-instance)
|
|
|
+const stateStore = new Map();
|
|
|
+
|
|
|
+// ─── OIDC config endpoint (pour le frontend) ────────────────────────────────
|
|
|
+
|
|
|
+router.get('/oidc/config', (req, res) => {
|
|
|
+ res.json({
|
|
|
+ enabled: !!(process.env.OIDC_ISSUER && process.env.OIDC_CLIENT_ID),
|
|
|
+ label: process.env.OIDC_BUTTON_LABEL || 'Se connecter via SSO',
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// ─── OIDC: initier le flux ──────────────────────────────────────────────────
|
|
|
+
|
|
|
+router.get('/oidc/login', async (req, res) => {
|
|
|
+ const client = await getOidcClient();
|
|
|
+ if (!client) return res.status(503).json({ error: 'SSO non configuré' });
|
|
|
+
|
|
|
+ const { generators } = require('openid-client');
|
|
|
+ const state = generators.state();
|
|
|
+ const nonce = generators.nonce();
|
|
|
+
|
|
|
+ stateStore.set(state, { nonce, createdAt: Date.now() });
|
|
|
+ // Nettoyer les états > 10 min
|
|
|
+ for (const [k, v] of stateStore) {
|
|
|
+ if (Date.now() - v.createdAt > 600_000) stateStore.delete(k);
|
|
|
+ }
|
|
|
+
|
|
|
+ const redirectUrl = client.authorizationUrl({
|
|
|
+ scope: process.env.OIDC_SCOPE || 'openid email profile',
|
|
|
+ state,
|
|
|
+ nonce,
|
|
|
+ });
|
|
|
+
|
|
|
+ res.redirect(redirectUrl);
|
|
|
+});
|
|
|
+
|
|
|
+// ─── OIDC: callback ─────────────────────────────────────────────────────────
|
|
|
+
|
|
|
+router.get('/oidc/callback', async (req, res) => {
|
|
|
+ const client = await getOidcClient();
|
|
|
+ if (!client) return res.status(503).json({ error: 'SSO non configuré' });
|
|
|
+
|
|
|
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const { generators } = require('openid-client');
|
|
|
+ const params = client.callbackParams(req);
|
|
|
+ const stored = stateStore.get(params.state);
|
|
|
+ if (!stored) return res.redirect(`${frontendUrl}/login?error=invalid_state`);
|
|
|
+ stateStore.delete(params.state);
|
|
|
+
|
|
|
+ const tokenSet = await client.callback(
|
|
|
+ process.env.OIDC_REDIRECT_URI || 'http://localhost:3001/api/auth/oidc/callback',
|
|
|
+ params,
|
|
|
+ { state: params.state, nonce: stored.nonce }
|
|
|
+ );
|
|
|
+
|
|
|
+ const claims = tokenSet.claims();
|
|
|
+ const sub = claims.sub;
|
|
|
+ const email = claims.email;
|
|
|
+ const name = claims.name || claims.preferred_username || email;
|
|
|
+ const firstName = claims.given_name || null;
|
|
|
+ const lastName = claims.family_name || null;
|
|
|
+
|
|
|
+ if (!sub) return res.redirect(`${frontendUrl}/login?error=no_sub`);
|
|
|
+
|
|
|
+ // Trouver ou créer l'utilisateur
|
|
|
+ let user = (await pool.query('SELECT * FROM users WHERE oidc_sub = $1', [sub])).rows[0];
|
|
|
+
|
|
|
+ if (!user && email) {
|
|
|
+ // Tenter de lier à un compte local existant avec le même email
|
|
|
+ user = (await pool.query('SELECT * FROM users WHERE email = $1', [email])).rows[0];
|
|
|
+ if (user) {
|
|
|
+ await pool.query('UPDATE users SET oidc_sub = $1 WHERE id = $2', [sub, user.id]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!user) {
|
|
|
+ // Créer un nouveau compte OIDC
|
|
|
+ const result = await pool.query(
|
|
|
+ 'INSERT INTO users (email, name, first_name, last_name, oidc_sub) VALUES ($1, $2, $3, $4, $5) RETURNING *',
|
|
|
+ [email || `oidc_${sub}@sso`, name, firstName, lastName, sub]
|
|
|
+ );
|
|
|
+ user = result.rows[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
|
|
|
+
|
|
|
+ // Rediriger vers le frontend avec le token dans le hash (évite les logs serveur)
|
|
|
+ res.redirect(`${frontendUrl}/auth/callback#token=${token}`);
|
|
|
+ } catch (err) {
|
|
|
+ console.error('OIDC callback error:', err.message);
|
|
|
+ res.redirect(`${(process.env.FRONTEND_URL || 'http://localhost:3000')}/login?error=oidc_failed`);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// ─── Auth locale ─────────────────────────────────────────────────────────────
|
|
|
+
|
|
|
router.post('/register', async (req, res) => {
|
|
|
const { email, password, name } = req.body;
|
|
|
if (!email || !password || !name)
|