【问题标题】:How to get users from Firebase auth based on custom claims?如何根据自定义声明从 Firebase 身份验证中获取用户?
【发布时间】:2019-02-27 18:03:39
【问题描述】:

我开始在我的 Firebase 项目中使用自定义声明来为我的应用实施基于角色的授权系统。

我将有一个 firebase-admin 脚本,它将为特定用户的 uid 设置 {admin: true}。这将帮助我编写更好、更清晰的 Firestore 安全规则。

admin.auth().setCustomUserClaims(uid, {admin: true})

到目前为止,一切都很好。我的问题是我还需要一个仪表板页面来让我知道哪些用户当前是我的应用程序中的管理员。

基本上,我需要一种方法来根据自定义声明查询/列出用户。有没有办法做到这一点?

From this answer, I can see that it's not possible to do this.

但也许,是否至少有一种方法可以检查(使用 Firebase 控制台)设置为特定用户的 customUserClaims

我当前的解决方案是将这些信息(管理员 uid's)存储在我的 Firestore 中的 admin-users 集合中,并通过我设置或撤销的任何管理员 customClaims 保持该信息是最新的。你能想出更好的解决方案吗?

【问题讨论】:

  • 你为什么不在数据库中创建一个用户节点然后写一个特定的属性,比如:“IsAdmin: true”?
  • 因为据我了解,这会使我的 Firestore 安全规则更加复杂。见This video。在允许对某些受保护数据进行读/写操作之前,我必须从数据库中的另一个位置(如您建议的位置)读取。
  • @cbdev420 实际上,您在问题中提到的 SO 问题/答案中描述的解决方案以及 Christophe 的提议并没有改变授权的实施方式:它仍然使用自定义声明(以及它们的所有优点) 但同时,您的用户列表 WITH 他们的声明存储在 Firebase 数据库中。
  • 这能回答你的问题吗? Firebase Auth - list of users by Custom Claims

标签: firebase google-cloud-firestore firebase-admin


【解决方案1】:

我最近解决了这个用例,将自定义声明作为“角色”数组字段复制到相应的 firestore 'users/{uid}/private-user/{data}' 文档中。在我的场景中,我必须区分两个角色(“admin”和“superadmin”)。 firestore 'users/' 集合的文档是公开的,而 'users/{uid}/private-user/' 集合的文档只能从拥有用户和“超级管理员”用户的客户端,或通过 Firestore Admin SDK(服务器端)也只能作为“超级管理员”用户。

此外,我只想允许“超级管理员”用户添加或删除“超级管理员”或“管理员”角色/声明;或获取“超级管理员”或“管理员”用户列表。

数据重复在 NoSQL 世界中非常普遍,并不被视为一种不好的做法。

这是我的代码 (Node.js/TypeScript)

首先,firebase 云功能实现(需要 Admin SDK)添加自定义用户声明/角色。

注意,“超级管理员”验证行

await validateUserClaim(functionName, context, "superadmin")

在创建至少一个“超级管理员”之前必须删除,以后可以使用该“超级管理员”向用户添加或删除其他角色/声明!

const functionName = "add-admin-user"

export default async (
  payload: string,
  context: CallableContext,
): Promise<void> => {
  try {
    validateAuthentication(functionName, context)
    validateEmailVerified(functionName, context)
    await validateUserClaim(functionName, context, "superadmin")
    const request = parseRequestPayload<AddAdminUserRoleRequest>(
      functionName,
      payload,
    )
    // Note, to remove a custom claim just use "{ [request.roleName]: null }"
    // as second input parameter.
    await admin
      .auth()
      .setCustomUserClaims(request.uid, { [request.roleName]: true })
    const userDoc = await db
      .collection(`users/${request.uid}/private-user`)
      .doc("data")
      .get()
    const roles = userDoc.data()?.roles ?? []
    if (roles.indexOf(request.roleName) === -1) {
      roles.push(request.roleName)
      db.collection(`users/${request.uid}/private-user`)
        .doc("data")
        .set({ roles }, { merge: true })
    }
  } catch (e) {
    throw logAndReturnHttpsError(
      "internal",
      `Firestore ${functionName} not executed. Failed to add 'admin' or ` +
      `'superadmin' claim to user. (${(<Error>e)?.message})`,
        `${functionName}/internal`,
      e,
     )
  }
}

其次,返回“超级管理员”或“管理员”用户列表的firebase云功能实现(需要Admin SDK)。

const functionName = "get-admin-users"

export default async (
  payload: string,
  context: CallableContext,
): Promise<GetAdminUsersResponse> => {
  try {
    validateAuthentication(functionName, context)
    validateEmailVerified(functionName, context)
    await validateUserClaim(functionName, context, "superadmin")
    const request = parseRequestPayload<GetAdminUsersRequest>(
      functionName,
      payload,
    )
    const adminUserDocs = await db
      .collectionGroup("private-user")
      .where("roles", "array-contains", request.roleName)
      .get()

    const admins = adminUserDocs.docs.map((doc) => {
      return {
        uid: doc.data().uid,
        username: doc.data().username,
        email: doc.data().email,
        roleName: request.roleName,
      }
    })
    return { admins }
  } catch (e) {
    throw logAndReturnHttpsError(
      "internal",
      `Firestore ${functionName} not executed. Failed to query admin users. (${
        (<Error>e)?.message
      })`,
      `${functionName}/internal`,
      e,
    )
  }
}

第三,验证辅助函数(需要 Admin SDK)。

export type AdminRoles = "admin" | "superadmin"

export const validateAuthentication = (
  functionName: string,
  context: CallableContext,
): void => {
  if (!context.auth || !context.auth?.uid) {
    throw logAndReturnHttpsError(
      "unauthenticated",
      `Firestore ${functionName} not executed. User not authenticated.`,
      `${functionName}/unauthenticated`,
    )
  }
}

export const validateUserClaim = async (
  functionName: string,
  context: CallableContext,
  roleName: AdminRoles,
): Promise<void> => {
  if (context.auth?.uid) {
    const hasRole = await admin
      .auth()
      .getUser(context.auth?.uid)
      .then((userRecord) => {
        return !!userRecord.customClaims?.[roleName]
      })
    if (hasRole) {
      return
    }
  }
  throw logAndReturnHttpsError(
    "unauthenticated",
    `Firestore ${functionName} not executed. User not authenticated as ` +
      `'${roleName}'. `,
    `${functionName}/unauthenticated`,
  )
}

export const validateEmailVerified = async (
  functionName: string,
  context: CallableContext,
): Promise<void> => {
  if (context.auth?.uid) {
    const userRecord = await auth.getUser(context.auth?.uid)
    if (!userRecord.emailVerified) {
      throw logAndReturnHttpsError(
        "unauthenticated",
        `Firestore ${functionName} not executed. Email is not verified.`,
        `${functionName}/email-not-verified`,
      )
    }
  }
}

最后,自定义声明只能在服务器端添加或删除,因为相应的“setCustomUserClaims”函数属于 firebase Admin SDK,而“get-admin-users”函数也可以在客户端实现。 Herehere 您将找到有关自定义声明的更多信息,包括受自定义用户声明/角色保护的客户端查询的 Firestore 规则。

【讨论】:

    猜你喜欢
    • 2021-06-30
    • 2020-06-28
    • 2022-01-20
    • 2016-04-08
    • 2020-04-01
    • 2022-12-10
    • 2022-01-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多