Closed abbaseya closed 3 years ago
Thanks for your suggestion @ixdguru !
Would you mind creating a new class ExampleLDAPSaltedSHA512PasswordEncryptor
with the decodedSalt
argument?
We're happy to review PRs.
Sure, here is the pull request: https://github.com/FusionAuth/fusionauth-example-password-encryptor/pull/3 That little change actually saved so much time during importing existing users from Gluu into FusionAuth. Here's how I had that figured out (assuming that a new plugin installed using ExampleLDAPSaltedSHA512PasswordEncryptor.java):
const fs = require('fs');
const https = require('https');
const FusionAuthHostname = 'app.fusionauth.io';
const FusionAuthAppID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
const FusionAuthTenantID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
const FusionAuthApiKey = 'XaNBV7xxxxxxxxxxxxxxxxxxxxxxxxxxxxNR';
var ldapExport = '/path/to/ldap/export/file';
if (fs.existsSync(ldapExport)) {
// build users array
var noSpaceProps = ['dn', 'oxTrustMetaLocation', 'userPassword', 'inum'];
var ignoreUsers = ['admin'];
var users = fs.readFileSync(ldapExport, 'utf-8').toString().split(/\n{2,}/g).filter(user => user != '').map(user => {
var m, pairs = {};
var multiLineValue = /(\w+):\s(.*\n\s.*)/gm;
while ((m = multiLineValue.exec(`${user}\n`)) !== null) {
if (m.index === multiLineValue.lastIndex) multiLineValue.lastIndex++;
m.shift();
var value = m[1].replace(/\n/g, '').trim();
if (noSpaceProps.includes(m[0])) value = value.replace(/\s+/g, '');
value = toJSON(value);
pairs[m[0]] = value;
}
var singleLineValue = /(\w+):\s(.+\n)/gm;
while ((m = singleLineValue.exec(`${user}\n`)) !== null) {
if (m.index === singleLineValue.lastIndex) singleLineValue.lastIndex++;
m.shift();
var value = m[1].replace(/\n/g, '').trim();
if (noSpaceProps.includes(m[0])) value = value.replace(/\s+/g, '');
value = toJSON(value);
if (!pairs[m[0]]) pairs[m[0]] = value;
}
return pairs;
}).filter(user => typeof user.userPassword != 'undefined' && !ignoreUsers.includes(user.uid));
// build fusionauth payload
var payload = {
users: users.map(user => {
var scheme = user.userPassword.match(/\{(\w+)\}/)[1].toLowerCase();
var encryptionScheme = '';
var ldapHash = '';
var saltEncoded = '';
var factor = 1;
switch (scheme) {
case 'bcrypt':
encryptionScheme = 'bcrypt';
ldapHash = user.userPassword.slice(37); // remaining characters after slat – encoded in base64
saltEncoded = user.userPassword.slice(15, 37); // salt - first 22 characters (16 bytes – 128 bits) after the algorithm (i.e. $2b$) and the cost (i.e. 08$) – already base64-encoded – https://en.wikipedia.org/wiki/Bcrypt
factor = Number(user.userPassword.slice(12, 14)); // get the cost rounds
break;
default:
encryptionScheme = 'salted-sha512'; // assuming that a new plugin installed using ExampleLDAPSaltedSHA512PasswordEncryptor.java
ldapHash = user.userPassword.slice(9);
var hashDecoded = Buffer.from(decodeURIComponent(ldapHash), 'base64').toString('binary'); // base64 decode
var saltDecoded = hashDecoded.slice(64); // salt - remaining string after the first 64 bytes (512 bits)
saltEncoded = Buffer.from(saltDecoded, 'binary').toString('base64'); // base64 salt
break;
}
var fusionUser = {
active: true,
verified: true,
registrations: [
{
applicationId: FusionAuthAppID,
verified: true
}
],
passwordChangeRequired: false,
password: ldapHash,
salt: saltEncoded,
encryptionScheme,
factor,
usernameStatus: 'ACTIVE',
username: user.uid,
firstName: user.givenName,
lastName: user.sn,
email: user.mail,
mobilePhone: user.oxTrustPhoneValue ? user.oxTrustPhoneValue.value : '',
};
return fusionUser;
})
};
loadUsers(payload).then(importUsers).then(() => {
console.log('Import complete!');
process.exit();
});
} else {
console.log('missing ldap export file!');
}
function loadUsers(payload) {
return new Promise(resolve => {
console.log('Fetch existing users ..');
request('users', {
path: '/api/user/search',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': FusionAuthApiKey,
'X-FusionAuth-TenantId': FusionAuthTenantID,
},
}, {
search: {
queryString: "*",
numberOfResults: 1000000,
startRow: 0,
sortFields: [
{
name: 'username',
order: 'asc'
}
]
}
}).then(users => {
users.forEach(user => {
var idx = payload.users.findIndex(u => u.username == user.username);
if (idx != -1) payload.users.splice(idx, 1);
});
resolve(payload);
});
});
}
function importUsers(payload) {
return new Promise(resolve => {
if (payload.users.length) {
console.log('Importing new users ..');
request('import', {
path: '/api/user/import',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': FusionAuthApiKey,
'X-FusionAuth-TenantId': FusionAuthTenantID,
},
}, payload).then(resolve);
} else {
resolve();
}
});
}
function request(key='', opts={}, payload={}) {
return new Promise(resolve => {
var postData = JSON.stringify(payload);
if (opts.method != 'GET') opts.headers['Content-Length'] = Buffer.byteLength(postData); // do not use postData.length when posting to java backend!
var req = https.request({
hostname: FusionAuthHostname,
port: 443,
rejectUnauthorized: false,
...opts
}, res => {
var chunks = [];
res.on('data', chunk => {
chunks.push(chunk);
if (key == 'import') process.stdout.write(data);
});
res.on('end', () => {
var data = Buffer.concat(chunks).toString();
if (key) {
try {
var ob = JSON.parse(data);
if (ob[key] || ['users', 'import'].includes(key)) {
resolve(ob[key] || []);
} else {
console.error(res.statusCode);
console.error(JSON.stringify(opts, null, 2));
console.error(JSON.stringify(ob, null, 2));
process.exit();
}
} catch (err) {
console.error(data);
process.exit();
}
} else {
resolve(res.statusCode);
}
});
});
req.on('error', err => {
console.error(err);
process.exit();
});
req.write(postData);
req.end();
});
}
function toJSON(input) {
try {
return JSON.parse(input);
} catch (err) {
return input;
}
}
LDAP algorithm to generate the userPassword value is
Base64Encode(SHA1(password+salt)+salt)
– https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html Hence the encryptor should return instead:https://github.com/FusionAuth/fusionauth-example-password-encryptor/blob/f6e9b2cbbc92d9cf932a718c4d777d5649636273/src/main/java/com/mycompany/fusionauth/plugins/ExampleSaltedSHA512PasswordEncryptor.java#L67