116 lines
3.6 KiB
JavaScript
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);
|
|
});
|