如何根据自定义声明从 Firebase 身份验证中获取用户?

cbd*_*per 5 firebase firebase-admin google-cloud-firestore

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

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

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

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

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

从这个答案中,我可以看到这是不可能的。

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

我当前的解决方案是将该信息(管理员uid's)存储admin-users在我的 Firestore 中的一个集合中,并使该信息与customClaims我设置或撤销的任何管理员保持同步。你能想到更好的解决方案吗?

Nil*_*mer 4

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

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

数据重复在 NoSQL 世界中非常常见,并且不被认为是一种不好的做法。

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

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

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

await validateUserClaim(functionName, context, "superadmin")
Run Code Online (Sandbox Code Playgroud)

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

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,
     )
  }
}
Run Code Online (Sandbox Code Playgroud)

其次,firebase云函数实现(需要Admin SDK)返回“superadmin”或“admin”用户的列表。

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,
    )
  }
}
Run Code Online (Sandbox Code Playgroud)

第三,验证辅助函数(需要管理 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`,
      )
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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