feat: implement Keycloak SSO integration with dynamic configuration and JIT provisioning
Some checks failed
Build & Push Docker Image to Gitea Registry / build-and-push (push) Failing after 17s

This commit is contained in:
Kroonk
2026-05-21 10:45:22 +02:00
parent d9334dae00
commit 163d8a20f1
4 changed files with 331 additions and 1 deletions

View File

@@ -6,6 +6,8 @@ const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const db = require('./database');
const crypto = require('crypto');
const { Issuer } = require('openid-client');
const app = express();
const JWT_SECRET = process.env.JWT_SECRET;
@@ -183,6 +185,116 @@ app.get('/api/auth/me', (req, res) => {
res.json(req.user);
});
// ==================== KEYCLOAK SSO API ====================
let oidcClient = null;
async function getOidcClient() {
if (oidcClient) return oidcClient;
const ssoAuthority = process.env.SSO_AUTHORITY || 'https://auth.mischlabs.de/realms/mischlabs';
const ssoClientId = process.env.SSO_CLIENT_ID || 'pocketbase';
const ssoClientSecret = process.env.SSO_CLIENT_SECRET;
const ssoRedirectUri = process.env.SSO_REDIRECT_URI || 'https://tom.mischlabs.de/api/auth/sso/callback';
if (!ssoClientSecret) {
throw new Error('SSO_CLIENT_SECRET is not configured');
}
const issuer = await Issuer.discover(ssoAuthority);
oidcClient = new issuer.Client({
client_id: ssoClientId,
client_secret: ssoClientSecret,
redirect_uris: [ssoRedirectUri],
response_types: ['code'],
});
return oidcClient;
}
app.get('/api/auth/sso/config', (req, res) => {
res.json({
enabled: !!(process.env.SSO_CLIENT_SECRET && process.env.SSO_CLIENT_ID && process.env.SSO_AUTHORITY),
});
});
app.get('/api/auth/sso/login', async (req, res) => {
try {
const client = await getOidcClient();
const authorizationUrl = client.authorizationUrl({
scope: 'openid email profile',
state: 'mischlabs-state',
});
res.redirect(authorizationUrl);
} catch (err) {
console.error('SSO Login Error:', err);
res.status(500).send('SSO Login Initialisierung fehlgeschlagen: ' + err.message);
}
});
app.get('/api/auth/sso/callback', async (req, res) => {
try {
const client = await getOidcClient();
const params = client.callbackParams(req);
const redirectUri = process.env.SSO_REDIRECT_URI || 'https://tom.mischlabs.de/api/auth/sso/callback';
const tokenSet = await client.callback(redirectUri, params, { state: 'mischlabs-state' });
const userinfo = await client.userinfo(tokenSet.access_token);
const username = userinfo.preferred_username || userinfo.name || userinfo.sub;
const email = userinfo.email;
if (!username) {
return res.status(400).send('Kein Benutzername im OIDC Token gefunden.');
}
db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
if (err) return res.status(500).send('Datenbankfehler');
const handleUserLogin = (dbUser) => {
const token = jwt.sign(
{ id: dbUser.id, username: dbUser.username, role: dbUser.role, created_at: dbUser.created_at },
JWT_SECRET, { expiresIn: '7d' }
);
res.cookie('token', token, { httpOnly: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 });
res.redirect('/admin.html');
};
if (user) {
handleUserLogin(user);
} else {
const randomPassword = crypto.randomBytes(32).toString('hex');
const hash = bcrypt.hashSync(randomPassword, 10);
db.get('SELECT COUNT(*) as count FROM users', [], (countErr, row) => {
let role = 'client';
if ((row && row.count === 0) || username.toLowerCase() === 'mrdiderot') {
role = 'admin';
}
db.run(
'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)',
[username, hash, role],
function (insertErr) {
if (insertErr) {
console.error('Failed JIT provisioning:', insertErr.message);
return res.status(500).send('Fehler bei der Benutzererstellung');
}
const newUserId = this.lastID;
db.get('SELECT * FROM users WHERE id = ?', [newUserId], (getErr, newUser) => {
if (getErr || !newUser) return res.status(500).send('Fehler beim Laden des neuen Benutzers');
handleUserLogin(newUser);
});
}
);
});
}
});
} catch (err) {
console.error('SSO Callback Error:', err);
res.status(500).send('SSO Authentifizierung fehlgeschlagen: ' + err.message);
}
});
app.post('/api/auth/change-password', requireAuth, (req, res) => {
const { old_password, new_password } = req.body;
if (!new_password || new_password.length < 4) {