将加盐的 sha512 密码从 symfony 2 迁移到 Firebase 身份验证

时间:2021-07-16 15:20:13

标签: firebase

我正在尝试将用户(包括密码)从旧的 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?

1 个答案:

答案 0 :(得分:0)

您尚未指定您的客户端应用程序使用的是什么,所以我只是假设它是一个将使用 Firebase Web SDK 的网络应用程序。

要使用此解决方案,您需要将 Symfony 用户数据迁移到私有 _migratedSymfonyUsers 集合下的 Firestore,其中每个文档都是该用户的电子邮件。

在客户端,过程将是:

  1. 从用户那里收集电子邮件和密码
  2. 尝试使用该电子邮件和密码组合登录 Firebase
  3. 如果失败,请使用该电子邮件和密码组合调用 Callable Cloud Function
  4. 如果函数返回成功消息(见下文),重新尝试使用给定的电子邮件和密码登录用户
  5. 适当处理成功/错误

在客户端,这看起来像:

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

在可调用的云函数中:

  1. (可选)使用 App Check
  2. 断言请求来自您的应用程序
  3. 断言已提供电子邮件和密码,如果没有则抛出错误。
  4. 断言给定的电子邮件存在于您迁移的用户中,如果不存在则抛出错误。
  5. 如果在迁移的用户中,对密码进行散列并与存储的散列进行比较。
  6. 如果哈希值不匹配,则抛出错误。
  7. 如果哈希匹配,请使用该电子邮件和密码组合创建一个新的 Firebase 用户
  8. 创建后,删除迁移的哈希并向调用者返回成功消息

在服务器上,这看起来像:

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