Files
ldap-to-oauth2/server.js
2025-10-08 11:12:59 -04:00

116 lines
3.6 KiB
JavaScript

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);
});