First commit
This commit is contained in:
115
server.js
Normal file
115
server.js
Normal file
@ -0,0 +1,115 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user