FusionAuth / fusionauth-example-password-encryptor

A example you can use to build a Password Encryptor Plugin for FusionAuth
https://fusionauth.io
Apache License 2.0
2 stars 1 forks source link

Suggested fix at ExampleSaltedSHA512PasswordEncryptor #2

Closed abbaseya closed 3 years ago

abbaseya commented 3 years ago

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:

return new String(Base64.getEncoder().encode(join(digest, decodedSalt)));

https://github.com/FusionAuth/fusionauth-example-password-encryptor/blob/f6e9b2cbbc92d9cf932a718c4d777d5649636273/src/main/java/com/mycompany/fusionauth/plugins/ExampleSaltedSHA512PasswordEncryptor.java#L67

mooreds commented 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.

abbaseya commented 3 years ago

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