Cannot authenticate users imported with SHA512 passwords
See original GitHub issue[READ] Step 1: Are you in the right place?
- For issues related to the code in this repository file a Github issue.
- If the issue pertains to Cloud Firestore, read the instructions in the “Firestore issue” template.
- For general technical questions, post a question on StackOverflow with the firebase tag.
- For general Firebase discussion, use the firebase-talk google group.
- For help troubleshooting your application that does not fall under one of the above categories, reach out to the personalized Firebase support channel.
[REQUIRED] Step 2: Describe your environment
- Operating System version: OS X 10.14.6
- Firebase SDK version: firebase-admin 8.9.2
- Firebase Product: auth
- Node.js version: 12.6.0
- NPM version: 6.9.0
[REQUIRED] Step 3: Describe the problem
Steps to reproduce:
I cannot authenticate with imported users using SHA512-encrypted passwords. The import is successful and the password hashes look correct using listUsers()
, but authentication fails with auth/wrong-password
.
What happened? How can we make the problem occur? This could be a description, log/console output, etc.
See code below for demo. I started from the [firebase docs instructions on importing users] (https://firebase.google.com/docs/auth/admin/import-users#import_users_with_md5_sha_and_pbkdf_hashed_passwords). Users imported using (known) SHA512 encrypted passwords are not able to authenticate.
I have tried this using both existing encrypted passwords using the AuthLogic ruby gem and by generating the encrypted passwords myself, as below. I have confirmed that my method produces identical password hashes to AuthLogic. Neither method is producing valid logins.
Relevant Code:
package.json dependencies:
{
"firebase": "^7.9.1",
"firebase-admin": "^8.9.2",
"js-base64": "^2.5.2",
"js-sha512": "^0.8.0"
}
Node script to reproduce the issue:
// ---- SETUP ---- //
const Base64 = require('js-base64').Base64;
const firebase = require('firebase');
const firebaseAdmin = require('firebase-admin');
const fs = require('fs')
const path = require('path');
const { sha512 } = require('js-sha512')
const util = require('util');
const adminCredsPath = path.join(process.env.HOME, 'etc', 'firebase', 'personal-admin.json');
const clientConfig = path.join(process.env.HOME, 'etc', 'firebase', 'personal-client.json');
// ---- HELPER FUNCTIONS ---- //
function encryptPassword({password, rounds, salt}) {
let c = password;
if (salt) { c += salt; }
for (let ii = 0; ii < rounds; ii++) { c = sha512(c) }
return c;
}
async function importUser({uid, email, encryptedPassword, salt, shaRounds}) {
const user = {
uid: uid,
email: email,
emailVerified: true,
passwordHash: Buffer.from(encryptedPassword),
};
if (salt) { user.passwordSalt = Buffer.from(salt) }
const importOptions = {
hash: {
algorithm: 'SHA512',
rounds: shaRounds
}
};
// allow repeatedly running the script without needing to use new uids
await firebaseAdmin.auth().deleteUser(uid).catch(err => console.log("Did not delete user:", err.message));
return firebaseAdmin.auth().importUsers(
[user], importOptions
);
}
function sleep(time) {
return new Promise((res, rejj) => {setTimeout(res, time)})
}
async function setupApp() {
firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(adminCredsPath)
});
const clientCreds = await util.promisify(fs.readFile)(clientConfig, 'utf-8').then(JSON.parse);
await firebase.initializeApp(clientCreds);
firebase.auth().onAuthStateChanged(user => {
console.log("Auth change:", user)
})
}
// ----- MAIN FUNCTIONS ---- //
async function main() {
await setupApp();
const email = 'test@foo.com';
const password = 'test123';
const rounds = 20;
// const salt = 'abc';
// const encryptedPassword = encryptPassword({password, rounds, salt});
const encryptedPassword = encryptPassword({password, rounds});
console.log({password, encryptedPassword, base64: Base64.encode(encryptedPassword)});
const userDetails = {uid: '123', email, encryptedPassword, shaRounds: rounds};
console.log("Importing", email);
const result = await importUser(userDetails);
console.log(result);
console.log("Created user for", email);
const {users} = await firebaseAdmin.auth().listUsers();
console.log("USERS", users.map(({uid, email, passwordHash, passwordSalt}) => ({uid, email, passwordHash, passwordSalt})));
await sleep(5000);
console.log("Attempting to sign in")
await firebase.auth().signInWithEmailAndPassword(email, password).catch(console.error)
}
main().then(() => { process.exit() })
OUTPUT:
{
password: 'test123',
encryptedPassword: 'ac888afb942d693a154323d180d708bc2b4c6d4902d3b90dfddc3e2e88d975a4c641addd492c67af24656cf4252c18fce8a45337d8c72e9a4f9b4e3782d90589',
base64: 'YWM4ODhhZmI5NDJkNjkzYTE1NDMyM2QxODBkNzA4YmMyYjRjNmQ0OTAyZDNiOTBkZmRkYzNlMmU4OGQ5NzVhNGM2NDFhZGRkNDkyYzY3YWYyNDY1NmNmNDI1MmMxOGZjZThhNDUzMzdkOGM3MmU5YTRmOWI0ZTM3ODJkOTA1ODk='
}
Importing test@foo.com
Auth change: null
{ successCount: 1, failureCount: 0, errors: [] }
Created user for test@foo.com
USERS [
{
uid: '123',
email: 'test@foo.com',
passwordHash: 'YWM4ODhhZmI5NDJkNjkzYTE1NDMyM2QxODBkNzA4YmMyYjRjNmQ0OTAyZDNiOTBkZmRkYzNlMmU4OGQ5NzVhNGM2NDFhZGRkNDkyYzY3YWYyNDY1NmNmNDI1MmMxOGZjZThhNDUzMzdkOGM3MmU5YTRmOWI0ZTM3ODJkOTA1ODk=',
passwordSalt: ''
}
]
Attempting to sign in
[M [Error]: The password is invalid or the user does not have a password.] {
code: 'auth/wrong-password',
message: 'The password is invalid or the user does not have a password.'
}
To prove that password hashes match the third-party output:
My method in Node:
> encryptPassword({password: "test123", rounds: 20})
'ac888afb942d693a154323d180d708bc2b4c6d4902d3b90dfddc3e2e88d975a4c641addd492c67af24656cf4252c18fce8a45337d8c72e9a4f9b4e3782d90589'
> encryptPassword({password: "test123", rounds: 20, salt: "abc"})
'1e878fcdb010cb6d4013a2bd4aa24e5b28eb0f7749b6c2429aa30c8bdf53af24d9f0055e8277196b25dff06845f85e2b99c44cf705d4556436c2abd023bef6ed'
Using AuthLogic in ruby:
> Authlogic::CryptoProviders::Sha512.encrypt("test123")
=> "ac888afb942d693a154323d180d708bc2b4c6d4902d3b90dfddc3e2e88d975a4c641addd492c67af24656cf4252c18fce8a45337d8c72e9a4f9b4e3782d90589"
> Authlogic::CryptoProviders::Sha512.encrypt("test123", "abc")
=> "1e878fcdb010cb6d4013a2bd4aa24e5b28eb0f7749b6c2429aa30c8bdf53af24d9f0055e8277196b25dff06845f85e2b99c44cf705d4556436c2abd023bef6ed"
I have tried the above script with salted and unsalted passwords. Same result.
Any help is greatly appreciated!
Issue Analytics
- State:
- Created 4 years ago
- Comments:8 (4 by maintainers)
I managed to get a successful sign in by modifying your function as follows:
In addition to using salt-first hashing, you should also account for the fact that the js-sha512 API you’re using returns output as a hex string.
I might be wrong, but it seems you’re setting the hash algorithm type to
SHA512
while actually using a PBKDF2 hash. I think the hashing logic for SHA512 should look something like this:That might explain why the login is not working as expected. As for why the password hash is not included in the
listUsers()
results, that may be due to your service account lacking some required IAM permission (I believe it needs to have the “Firebase Authentication Admin” role for those fields to be returned – See #660).