const ldap = require('ldapjs'); const axios = require('axios'); // Configuration const LDAP_PORT = 3893; const KEYCLOAK_URL = ''; const KEYCLOAK_REALM = ''; const KEYCLOAK_CLIENT_ID = ''; const KEYCLOAK_CLIENT_SECRET = ''; const BASE_DN = ''; // Create LDAP server const server = ldap.createServer(); // Helper function to parse DN and extract username function extractUsername(dn) { try { const parsed = ldap.parseDN(dn); // rdns is an array in some versions, need to handle it properly if (parsed.rdns && Array.isArray(parsed.rdns)) { for (const rdn of parsed.rdns) { if (rdn.attrs) { for (const attr of rdn.attrs) { if (attr.type === 'cn' || attr.type === 'uid') { return attr.value; } } } } } // Fallback: try to extract from string const match = dn.match(/(?:cn|uid)=([^,]+)/i); return match ? match[1] : null; } catch (error) { console.error('Error parsing DN:', error); // Fallback: simple regex extraction const match = dn.toString().match(/(?:cn|uid)=([^,]+)/i); return match ? match[1] : null; } } // Authenticate against Keycloak using Resource Owner Password Credentials flow async function authenticateWithKeycloak(username, password) { try { const tokenUrl = `${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`; const params = new URLSearchParams(); params.append('grant_type', 'password'); params.append('client_id', KEYCLOAK_CLIENT_ID); params.append('client_secret', KEYCLOAK_CLIENT_SECRET); params.append('username', username); params.append('password', password); const response = await axios.post(tokenUrl, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); return response.status === 200 && response.data.access_token; } catch (error) { console.error(`Auth failed for ${username}:`, error.response?.data || error.message); return false; } } // Handle BIND requests (authentication) server.bind(BASE_DN, async (req, res, next) => { const username = extractUsername(req.dn.toString()); const password = req.credentials; console.log(`Bind attempt for: ${username}`); if (!username || !password) { console.log('Missing username or password'); return next(new ldap.InvalidCredentialsError()); } const authenticated = await authenticateWithKeycloak(username, password); if (authenticated) { console.log(`Successfully authenticated: ${username}`); res.end(); return next(); } else { console.log(`Authentication failed for: ${username}`); return next(new ldap.InvalidCredentialsError()); } }); // Handle SEARCH requests (required by some LDAP clients) server.search(BASE_DN, (req, res, next) => { console.log(`Search request: ${req.dn.toString()}, filter: ${req.filter.toString()}`); // Return empty results - we only care about authentication res.end(); return next(); }); // Handle UNBIND server.unbind((req, res, next) => { res.end(); return next(); }); // Start server server.listen(LDAP_PORT, '0.0.0.0', () => { console.log(`LDAP-OAuth2 bridge listening on port ${LDAP_PORT}`); console.log(`Keycloak URL: ${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}`); }); // Error handling server.on('error', (err) => { console.error('Server error:', err); });