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
Some checks failed
Build & Push Docker Image to Gitea Registry / build-and-push (push) Failing after 17s
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user