我正在尝试将用户(包括密码)从旧的 symfony 2 应用程序迁移到 firebase 身份验证(或谷歌身份平台)。
在 symfony2 应用程序中,用户的密码使用 sha512 和盐进行散列。我已经在 firebase (https://firebase.google.com/docs/auth/admin/import-users) 的文档中发现可以使用他们的密码和哈希导入用户。然而,firebase 使用的 sha512 哈希似乎与 symfony 使用的不同。
对于旧的 symfony 项目,使用以下配置:
security:
encoders:
FOS\UserBundle\Model\UserInterface: sha512
通过查看源代码,我发现 symfony 给定一个 salt 和一个密码 symfony 会产生这样的哈希:(在 python 代码中)
def get_hash(salt, password):
hash = password.encode('utf-8')
salted = hash + salt
hash = hashlib.sha512(salted).digest()
for i in range(1, 5000):
# symfony keeps adding salted for every iteration, this is something firebase does not it seems
hash = hashlib.sha512(hash + salted).digest()
return base64.b64encode(hash).decode('utf-8')
但是,当我像下面的代码一样导入它时,此代码不允许我登录。然而,它确实产生了与我在 symfony2 应用程序的数据库中所拥有的相同的哈希值:
app = firebase_admin.initialize_app()
salt = '{test}'.encode('utf-8')
hash = get_hash(salt=salt, password='xyz')
print('calculated hash', base64.b64encode(hash))
users = [
auth.ImportUserRecord(
uid='foobar',
email='foo@bar.com',
password_hash=hash,
password_salt=salt
)
]
hash_alg = auth.UserImportHash.sha512(rounds=5000)
try:
result = auth.import_users(users, hash_alg=hash_alg)
for err in result.errors:
print('Failed to import user:', err.reason)
except exceptions.FirebaseError as error:
print('Error importing users:', error)
但是,当我使用以下功能时,我可以使用密码登录。
def get_hash(salt, password):
hash = password.encode('utf-8')
salted = salt + hash
hash = hashlib.sha512(salted).digest()
for i in range(1, 5000):
hash = hashlib.sha512(hash).digest()
return hash
我已经找到了一种方法来更改添加盐的顺序,但是我无法在 firebase hash = hashlib.sha512(hash + salted).digest()
中找到像这样散列的方法。
现在似乎没有办法将我的密码迁移到 firebase,因为 symfony 的实现与 firebase 使用的有点不同。有没有人知道一种方法来确保我仍然可以导入我当前的哈希?这会很棒。
如果没有,有什么替代的解决方法?
是否可以让 firebase 向我自己的端点发出请求以验证密码。
另一种方法是尝试捕获登录过程并将其发送到我自己的端点,在后台设置密码,然后将请求发送到 firebase?
答案 0 :(得分:0)
您尚未指定您的客户端应用程序使用的是什么,所以我只是假设它是一个将使用 Firebase Web SDK 的网络应用程序。
要使用此解决方案,您需要将 Symfony 用户数据迁移到私有 _migratedSymfonyUsers
集合下的 Firestore,其中每个文档都是该用户的电子邮件。
在客户端,过程将是:
在客户端,这看起来像:
const legacySignIn = firebase.functions().httpsCallable('legacySignIn');
async function doSignIn(email, password) {
try {
return await firebase.auth()
.signInWithEmailAndPassword(email, password);
} catch (fbError) {
if (fbError.code !== "auth/user-not-found")
return Promise.reject(fbError);
}
// if here, attempt legacy sign in
const response = await legacySignIn({ email, password });
// if here, migrated successfully
return firebase.auth()
.signInWithEmailAndPassword(email, password);
}
// usage:
doSignIn(email, password)
.then(() => console.log('successfully logged in/migrated'))
.catch((err) => console.error('failed to log in', err));
在可调用的云函数中:
在服务器上,这看起来像:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
function symfonyHash(pwd, salt) {
// TODO: Hash function
return /* calculatedHash */;
}
exports.legacySignIn = functions.https.onCall(async (data, context) => {
if (context.app == undefined) { // OPTIONAL
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called from an App Check verified app.');
}
if (!data.email || !data.password) {
throw new functions.https.HttpsError(
'invalid-argument',
'An email-password combination is required');
}
if (data.email.indexOf("/") > -1) {
throw new functions.https.HttpsError(
'invalid-argument',
'Email contains forbidden character "/"');
}
const migratedUserSnapshot = await admin.firestore()
.doc(`_migratedSymfonyUsers/${data.email}`);
if (!migratedUserSnapshot.exists) {
throw new functions.https.HttpsError(
'not-found',
'No user matching that email address was found');
}
const storedHash = migratedUserSnapshot.get("hash");
const calculatedHash = symfonyHash(password, salt);
if (storedHash !== calculatedHash) {
throw new functions.https.HttpsError(
'permission-denied',
'Given credential combination doesn\'t match');
}
// if here, stored and calculated hashes match, migrate user
// get migrated user data
const { displayName, roles } = migratedUserSnapshot.data();
// create the user based on migrated data
const newUser = await admin.auth().createUser({
email,
password,
...(displayName ? { displayName } : {})
});
if (roles) { // <- OPTIONAL
const roleMap = {
"symfonyRole": "tokenRole",
"USERS_ADMIN": "isAdmin",
// ...
}
const newUserRoles = [];
roles.forEach(symfonyRole => {
if (roleMap[symfonyRole]) {
newUserRoles.push(roleMap[symfonyRole]);
}
});
if (newUserRoles.length > 0) {
// migrate roles to user's token
await setCustomUserClaims(
newUser.uid,
newUserRoles.reduce((acc, r) => { ...acc, [r]: true }, {})
);
}
}
// remove the old user data now that we're done with it.
await hashSnapshot.ref.delete();
// return success to client
return { success: true };
});