使用身份平台的用户授权的最终指南
#node #nextjs #go #googlecloud

最近,我将用户身份验证和基于角色的授权(又称RBAC)添加到我的一个项目中,以便可以授予某些用户 管理员并访问我们的内部工具。

当我的服务器在Google Cloud平台上运行时,我决​​定使用 Google Identity Platform (GIP),这与 firebase Authentication的服务本质上是相同的,具有身份平台 (考虑到类固醇的壁炉身份验证)。

Google不是涵盖这种常见的流程的适当文档或连贯的指南,而是将我的概念和重叠的库交给了我。

因此,我决定通过为您提供所需的(Ultimate)指南来节省您的时间,以涵盖完整的用户auth/authz流。

议程

计划

Full Diagram

我将向您展示以下流的实现:

  1. 用户通过Web客户端签署(或IN)
  2. Google身份平台(GIP)验证请求并触发阻止功能
  3. 阻止功能基于电子邮件地址分配角色(管理员或用户)
  4. 如果用户是管理员,则将其重定向到管理页面
  5. 如果未经授权的用户直接进入管理页面
  6. 将它们重定向回家
  7. 针对后端路线提出请求
  8. 路线触发auth Middleware
  9. auth Middleware验证用户针对GIP
  10. 如果存在用户,则称为AUTHZ(授权)中间件
  11. 如果用户的角色足以继续,则处理处理程序
  12. 处理程序响应

选择很重要

由于我不知道您使用的是哪种技术堆栈,因此我决定进行多个实现。您会提供不同的选择,这些选择会影响您阅读的内容并更适合您的确切情况。

代码

不管您的选择如何,所有代码都可以在this repo中找到。

您可以从 react (下一个13)或 vanilla javascript for前端, go node 后端Middlewares。

但是,请记住,本指南的目标不是 开发完美的业务逻辑,而是阐明组装完整的auth/authz流量所需的各种概念和组件。

如果您在遵循本指南时遇到任何问题,请在x.com(Twitter)上滑到我的DMS或打开GitHub问题,我会帮助。

设置

首先,启用身份平台,然后添加您的第一个提供商。

提供商
提供商是您的用户可以选择注册或登录的服务,通常是第三方的服务。示例:Google,电子邮件 /密码,苹果,< / p>

  1. 转到您的GCP console,然后打开或添加一个项目
  2. Enable the Identity Platform
  3. 单击提供商选项卡
  4. 单击添加提供商按钮

对于本指南,我创建了两个实现:Google和电子邮件/密码,以说明最常见情况的流程。如果您选择的提供商不同,您仍然可以跟随Google实施,因为其他提供商的流量相似。

选择提供商
Sign in with Google
Sign in with Email / Password

与Google登录

添加提供商

  1. 从下拉列表中,选择 Google 作为提供商
  2. Web SDK配置下,您必须提供ID和秘密
    1. 单击 API和服务页面链接或搜索(通过按/ apis and Services
    2. 凭据选项卡下,您将参见 oauth 2.0客户端ID 部分。单击 Web客户端
    3. 找到并粘贴客户ID 秘密在右侧
  3. 跳过允许客户ID
  4. 单击配置屏幕按钮。这将打开 oauth同意屏幕
    1. 填写用户支持电子邮件
    2. 开发人员联系信息,添加您的电子邮件地址
    3. 您可以忽略其他所有内容(包括应用程序名称)。在任何时候,您都可以回来编辑内容
    4. 单击保存并继续
    5. 您不必继续;仅需要步骤#1。稍后,当您准备好公开发布时,您会回来并正确填写所有内容。现在,返回提供商设置
  5. 将开发客户端应用程序开发到授权域部分(位于页面的右侧)时,将使用您使用的域。默认情况下, localhost 已经存在,但是如果您在开发过程中使用 127.0.0.1 ,请确保添加它。此外,如果您已经知道您的公众(产品)域,请立即添加
  6. 单击保存。您现在将看到 Google提供商 启用

添加客户端应用

让我们添加一个简单的客户端,以便用户可以注册和进入。现在,这是Google Cloud Platform和Firebase开始重叠的地方。

Intersection GCP and Firebase

Google身份平台(GIP)与Identity Platform 共享与 Firebase身份验证相同的后端。

由于这两种是相同的产品,因此Google决定不是在某些地方复制自己,并要求您使用他们的 firebase客户client sdk 。我已经为您准备了两种不同的实现,其中一个在 React (使用下一个13)中,一个在 vanilla JS 中。在下面选择一个。

选择客户
React / Next Client
Vanilla JS Client

react /下一个客户

我将在此处使用react.js 13作为示例,但是您可以在没有下一步的情况下使用代码并实现相同的功能。最后,这只是一个简单的React JavaScript代码。

  1. Run npx create-next-app@latest
  2. 安装firebase客户端SDK npm i firebase
  3. 添加文件 firebaseconfig.js app 文件夹
  4. 返回身份平台的仪表板,下面提供商 tab单击应用程序设置详细信息复制 apikey authdomain

  5. 将以下内容添加到您的 firebaseconfig.js

    import { initializeApp } from 'firebase/app';
    import { getAuth } from 'firebase/auth';
    
    const firebaseConfig = {
      apiKey: '<your apiKey>',
      authDomain: '<your authDomain>',
    }
    
    const app = initializeApp(firebaseConfig);
    export const auth = getAuth(app);
    
  6. 更改您的 page.js 包括此功能

    ð - 如果找不到模块:可以解决编码 - 错误,请运行npm i -D encoding

    'use client'; // <--- Next.js 13 Client Component declaration. Don't add if you don't use Next
    import { auth } from './firebaseConfig';
    import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
    import { useRouter } from 'next/navigation'; // <--- Next-specific Router. Use your own React Router if you don't use Next
    
    export default function Home() {
      const provider = new GoogleAuthProvider();
      const router = useRouter();
    
        const signInWithGoogle = async () => {
        try {
          await signInWithPopup(auth, provider);
    
          // Print token to test the middlewares later on via HTTP client
          // console.log(await auth.currentUser.getIdToken(true));
    
          const { claims } = await auth.currentUser.getIdTokenResult(true);
          if (claims.role === 'admin') router.push('/admin');
        } catch (error) {
          console.log(error);
        }
      }
    
      return (
        <div>
          <button onClick={signInWithGoogle}>Sign in with Google</button>
        </div>
      )
    }
    
    1. 按下按钮,您将被重定向到Google弹出窗口,您可以在其中选择您的帐户。然后,用户是同时创建和签名的
    2. 完成注册人群auth.currentUser。然后,您可以获取当前用户(JWT)令牌并解码以检索用户的数据,包括索赔

      主张
      索赔只是有关用户的数据。它们与授权流有关,因为在后端身份验证期间,您可以在用户上设置自定义索赔,以稍后检索以确定其访问级别。

    3. 角色在上面的代码段中是自定义主张,您将在本指南中稍后设置。如果用户的角色主张设置为 admin ,则将其重定向到专用于Admins的React页面

  7. 创建一个名为 admin 的新目录,并在其中创建一个名为 page.js 的新文件。然后,添加以下代码

    'use client'; // <--- Next.js 13 Client Component declaration. Don't add if you don't use Next
    import { auth } from '../firebaseConfig';
    import { useRouter } from 'next/navigation';
    import { useEffect } from 'react';
    
    export default function Admin() {
      const router = useRouter();
    
      useEffect(() => {
        auth.onAuthStateChanged(async (user) => {
          if (!user) router.push('/');
    
          const { claims } = await user.getIdTokenResult(true);
          if (claims.role !== 'admin') router.push('/');
        });
      }, []);
    
      return (
        <div>
          <h1>Admin</h1>
        </div>
      )
    }
    

    firebase auth触发回调函数的时刻,您只需检查用户是否存在,以及角色索赔是否设置为 admin 。如果没有,您想将这样的访问者重定向到主页,而不是显示管理视图。这主要用于将访问者直接转到此页面而无需先验证。

现在,跳到您将发现如何为用户分配角色的部分。

Jump to Assign Roles Section

香草JS客户

让我们使用一个简单的客户端,使用嵌入式的香草JS作为脚本标签html

  1. 返回身份平台的仪表板,下面提供商 tab单击应用程序设置详细信息。复制 apikey authdomain
  2. 创建一个新的HTML文件,称为 auth-google.html ,然后在关闭的主体标签之前插入以下脚本

    <!-- ... -->
    <body>      
            <!-- ... your other tags should be above ... -->
        <button onclick="signInWithGoogle()">Sign in with Google</button>
    
        <script src="https://www.gstatic.com/firebasejs/8.0/firebase.js"></script>
        <script>
            var config = {
                apiKey: "<your apiKey>",
                authDomain: "<your authDomain>",
            };
            firebase.initializeApp(config);
        </script>
        <script>
            function signInWithGoogle() {
                const provider = new firebase.auth.GoogleAuthProvider();
    
                firebase.auth().signInWithPopup(provider)
                // Print token to test the middlewares later on via HTTP client
                /* .then(() => {
                    firebase.auth().currentUser.getIdToken(true)
                    .then(token => console.log(token))
                }) */
                .then(() =>
                    firebase.auth().currentUser.getIdTokenResult(true)
                    .then(result => {
                        if (result.claims.role === 'admin') {
                            window.location.href = 'auth-google-admin.html';
                        }
                    })
                    .catch(error => console.log(error))
                )
                .catch(error => console.log(error));
            }
        </script>
    </body>
    <!-- ... -->
    
    1. 按下按钮,您将被重定向到Google弹出窗口,您可以在其中选择您的帐户。然后,用户是同时创建和签名的
    2. 完成注册人群auth.currentUser。然后,您可以获取当前用户(JWT)令牌并将其解码以检索用户数据(上面的代码段中的result),包括索赔

      主张
      索赔只是有关用户的数据。它们与授权流有关,因为在对后端中的用户进行身份验证时,您可以在用户向用户设置 custom 索赔,后来可以检索以确定访问级别。

    3. 角色在上面的代码段中是自定义主张,您将在本指南中稍后设置。如果用户的角色主张设置为 admin ,则将将其重定向到专门为Admins

    4. 的页面
  3. 在同一目录中,创建一个名为 auth-google-admin.html 的新文件,然后添加以下代码

    <!-- ... -->
    <body>
        <h1>Admin</h1>
    
        <script src="https://www.gstatic.com/firebasejs/8.0/firebase.js"></script>
        <script>
            var config = {
                apiKey: "<your apiKey>",
                authDomain: "<your authDomain>",
            };
            firebase.initializeApp(config);
        </script>
        <script>
            function checkRole() {
                firebase.auth().onAuthStateChanged(user => {
                    if (!user) {
                        window.location.href = 'auth-google.html';
                    } else {
                        // Print token to test the middlewares later on via HTTP client
                        /* firebase.auth().currentUser.getIdToken(true)
                        .then(token => console.log(token))
                        .catch(error => console.log(error)); */
    
                        user.getIdTokenResult(true)
                        .then(result => {
                            if (result.claims.role !== 'admin') {
                                window.location.href = 'auth-google.html';
                            }
                        })
                        .catch(error => console.log(error));
                    }
                });
            }
            window.onload = checkRole;
        </script>
    </body>
    <!-- ... -->
    

    firebase auth触发回调函数的时刻,您只需检查用户是否存在,以及角色索赔是否设置为 admin 。如果没有,您想将这样的访客重定向到 auth-google.html 页面,而不是显示管理员视图。这主要用于将访问者直接转到此页面而无需先验证。

现在,跳到您将发现如何为用户分配角色的部分。

Jump to Assign Roles Section

使用电子邮件 /密码登录

添加提供商

  1. 从下拉列表中,选择并启用电子邮件/密码作为提供商
  2. 取消选中
  3. 忽略模板,因为您可以随时在
  4. 上调整其内容
  5. 将开发客户端应用程序开发到授权域部分(位于页面的右侧)时,将使用您使用的域。默认情况下, localhost 已经存在,但是如果您在开发过程中使用 127.0.0.1 ,请确保添加它。此外,如果您已经知道您的公众(产品)域,请立即添加
  6. 单击保存。您现在将看到电子邮件/密码 提供商 启用

添加客户端应用

让我们添加一个简单的客户端,以便用户可以注册和进入。现在,这是Google Cloud Platform和Firebase开始重叠的地方。

Intersection GCP and Firebase

Google身份平台(GIP)与Identity Platform 共享与 Firebase身份验证相同的后端。

由于这两种是相同的产品,因此Google决定不是在某些地方复制自己,并要求您使用他们的 firebase客户client sdk 。我已经为您准备了两种不同的实现,其中一个在 React (使用下一个13)中,一个在 vanilla JS 中。在下面选择一个。

选择客户
React / Next Client
Vanilla JS Client

react /下一个客户

我将在此处使用react.js 13作为示例,但是您可以在没有下一步的情况下使用代码并实现相同的功能。最后,这只是一个简单的React JavaScript代码。

  1. Run npx create-next-app@latest
  2. 安装firebase客户端SDK npm i firebase
  3. 添加文件 firebaseconfig.js app 文件夹
  4. 返回到身份平台的仪表板,下面提供商 tab单击应用程序设置详细信息。复制 apikey authdomain

  5. 将以下内容添加到您的 firebaseconfig.js

    import { initializeApp } from 'firebase/app';
    import { getAuth } from 'firebase/auth';
    
    const firebaseConfig = {
      apiKey: '<your apiKey>',
      authDomain: '<your authDomain>',
    }
    
    const app = initializeApp(firebaseConfig);
    export const auth = getAuth(app);
    
  6. 更改您的 page.js 包括此简单功能

    ð - 如果找不到模块:可以解决编码 - 错误,请运行npm i -D encoding

    'use client'; // <--- Next.js 13 Client Component declaration. Don't add if you don't use Next
    import { useState } from 'react';
    import { auth } from './firebaseConfig';
    import {
      createUserWithEmailAndPassword,
      signInWithEmailAndPassword,
      sendEmailVerification,
      signOut
    } from 'firebase/auth';
    import { useRouter } from 'next/navigation'; // <--- Next-specific Router. Use your own React Router if you don't use Next
    
    export default function Home() {
      const router = useRouter();
    
      // ... States omitted (code too long). [Click here for full code](https://github.com/MichalMoravik/google-identity-guide/blob/main/client-react-next/auth-email/app/page.js)
    
      const registerWithEmail = async (email, password) => {
        try {
          const cred = await createUserWithEmailAndPassword(auth, email, password);
    
                // Change the redirect URL to env var if you want
          await sendEmailVerification(cred.user, { url: 'http://localhost:3000' });
          setMsg('Please verify your email before signing in');
    
          await signOut(auth);
        } catch (err) {
          console.log('Unexpected error: ', err);
        }
      }
    
      const signInWithEmail = async (email, password) => {
        try {
          await signInWithEmailAndPassword(auth, email, password);
    
          // Print token to test the middlewares later on via HTTP client
          // console.log(await auth.currentUser.getIdToken(true));
    
          const { claims } = await auth.currentUser.getIdTokenResult(true);
    
          if (!claims.email_verified) {
                    // Change the redirect URL to env var if you want
            await sendEmailVerification(auth.currentUser, { url: 'http://localhost:3000' });
            setMsg('Please verify your email before signing in. We have sent you another verification email');
            await signOut(auth);
            return;
          }
    
          console.log('Signed in as: ', claims.email);
    
          if (claims.role === 'admin') router.push('/admin');
        } catch (err) {
          console.log('Unexpected error: ', err);
        }
      }
    
      return (
        <div>
            { //... HTML tags omitted (code too long). [Click here for full code](https://github.com/MichalMoravik/google-identity-guide/blob/main/client-react-next/auth-email/app/page.js) }
        </div>
      )
    }
    

    registerwithemail

    1. 执行createUserWithEmailAndPassword()时,用户是同时创建和签名的
    2. 此后,sendEmailVerification()发送电子邮件以验证其地址。您可以通过编辑提供商设置来更改其内容

      在第一次登录之前发送验证电子邮件
      您可能会问,我可以在用户首次登录之前发送验证电子邮件吗?答案是否定的。我上面提出的是实现这一结果的唯一方法。 I elaborated on this problem in this StackOverflow question

    3. 通常,您希望用户在签署电子邮件之前验证其电子邮件地址。由于已签署(由于步骤#1中的注册),您必须将其签名

    ðâ提示:将来,您可能需要考虑添加recaptcha

    signwithemail

    1. 按下按钮,您的用户将被登录。完成注册populate import admin from 'firebase-admin'; admin.initializeApp({ credential: admin.credential.applicationDefault(), projectId: process.env.GCP_PROJECT_ID, }); const authClient = admin.auth(); export { authClient };
    2. 然后,您可以获取当前用户(JWT)令牌并使用getIdTokenResult()进行解码以检索用户的数据,包括索赔

      主张
      索赔只是有关用户的数据。它们与授权流有关,因为在后端身份验证期间,您可以在用户上设置自定义索赔,以稍后检索以确定其访问级别。

    3. 如果用户尚未验证其地址,则再次向他们发送电子邮件,并让他们知道他们必须先验证它

      ð注意
      任何人都可以修改您的客户端代码,因此检查地址是否已验证是否在此级别上不提供任何安全性;客户实现仅具有UX目的。您将在后一部分中保护数据(保护数据)。

    4. 最后,您检查用户的角色是否设置为 admin 。这个角色自定义声称您将在本指南中稍后设置。如果用户的角色 admin ,则将其重定向到专用于Admins的React页面

  7. 创建一个名为 admin 的新目录,并在其中创建一个名为 page.js 的新文件。然后,添加以下代码

    'use client'; // <--- Next.js 13 Client Component declaration. Don't add if you don't use Next
    import { auth } from '../firebaseConfig';
    import { useRouter } from 'next/navigation';
    import { useEffect } from 'react';
    
    export default function Admin() {
      const router = useRouter();
    
      useEffect(() => {
        auth.onAuthStateChanged(async (user) => {
          if (!user) router.push('/');
    
          const { claims } = await user.getIdTokenResult(true);
          if (claims.role !== 'admin') router.push('/');
        });
      }, []);
    
      return (
        <div>
          <h1>Admin</h1>
        </div>
      )
    }
    

    firebase auth触发回调函数的时刻,您只需检查用户是否存在,以及角色索赔是否设置为 admin 。如果没有,您想将这样的访问者重定向到主页,而不是显示管理视图。这主要用于将访问者直接转到此页面而无需先验证。

现在,跳到您将发现如何为用户分配角色的部分。

Jump to Assign Roles Section

香草JS客户

让我们使用一个简单的客户端,使用嵌入式的香草JS作为脚本标签html

  1. 返回到身份平台的仪表板,下面提供商 tab单击应用程序设置详细信息。复制 apikey authdomain

  2. 创建一个名为 auth-email.html 的新的HTML文件,然后在关闭的主体标签之前插入以下脚本

    <!-- ... -->
    <body>      
            <!-- ... HTML tags omitted (code too long). [Click here for full code](https://github.com/MichalMoravik/google-identity-guide/blob/main/client-js/auth-email/auth-email.html) ... -->
    
        <script src="https://www.gstatic.com/firebasejs/8.0/firebase.js"></script>
        <script>
          var config = {
            apiKey: "<your apiKey>",
            authDomain: "<your authDomain>",
          };
          firebase.initializeApp(config);
        </script>
        <script>
            // ... Listeners omitted (code too long). [Click here for full code](https://github.com/MichalMoravik/google-identity-guide/blob/main/client-js/auth-email/auth-email.html)
    
            function registerWithEmail(email, password) {
                firebase.auth().createUserWithEmailAndPassword(email, password)
                .then(cred => {
                                    // Change the redirect URL to env var if you want
                    return cred.user.sendEmailVerification({
                        url: 'http://127.0.0.1:8080/auth-email.html'
                    })
                })
                .then(() => {
                    const msgTag = document.querySelector('#msg');
                    msgTag.innerHTML = `Please verify your email before signing in`;
    
                    return firebase.auth().signOut()
                })
                .catch(err => console.log('Unexpected error: ', err));
            }
    
            function signInWithEmail(email, password) {
                firebase.auth().signInWithEmailAndPassword(email, password)
                // Print token to test the middlewares later on via HTTP client
                /*.then(() => {
                    firebase.auth().currentUser.getIdToken(true)
                    .then(token => console.log(token))
                })*/
                .then(() => firebase.auth().currentUser.getIdTokenResult(true))
                .then(result => {
                    if (!result.claims.email_verified) {
                        return firebase.auth().currentUser.sendEmailVerification({
                                                    // Change the redirect URL to env var if you want
                            url: 'http://127.0.0.1:8080/auth-email.html'
                        })
                        .then(() => {
                            const msgTag = document.querySelector('#msg');
                            msgTag.innerHTML = `Please verify your email before
                            signing in. We have sent you another verification email`;
    
                            return firebase.auth().signOut()
                        })
                    }
    
                    console.log('Signed in as: ', result.claims.email);
    
                    if (result.claims.role === 'admin') {
                        window.location.href = 'auth-email-admin.html';
                    }
                })
                .catch(err => console.log('Unexpected error: ', err));
            }
        </script>
    </body>
    <!-- ... -->
    

    registerwithemail

    1. 执行createUserWithEmailAndPassword()时,用户是同时创建和签名的
    2. 此后,sendEmailVerification()发送电子邮件以验证其地址。您可以通过编辑提供商设置来更改其内容

      在第一次登录之前发送验证电子邮件
      您可能会问,我可以在用户首次登录之前发送验证电子邮件吗?答案是否定的。我上面提出的是实现这一结果的唯一方法。 I elaborated on this problem in this StackOverflow question

    3. 通常,您希望用户在签署电子邮件之前验证其电子邮件地址。由于已签署(由于步骤#1中的注册),您必须将其签名

    ðâ提示:将来,您可能需要考虑添加recaptcha

    signwithemail

    1. 按下按钮,您的用户将被登录。完成注册populate import admin from 'firebase-admin'; admin.initializeApp({ credential: admin.credential.applicationDefault(), projectId: process.env.GCP_PROJECT_ID, }); const authClient = admin.auth(); export { authClient };
    2. 然后,您可以获取当前用户(JWT)令牌并使用getIdTokenResult()进行解码以检索用户的数据,包括索赔

      主张
      索赔只是有关用户的数据。它们与授权流有关,因为在后端身份验证期间,您可以在用户上设置自定义索赔,以稍后检索以确定其访问级别。

    3. 如果用户尚未验证其地址,则再次向他们发送电子邮件,并让他们知道他们必须先验证它

      ð注意
      任何人都可以修改您的客户端代码,因此检查地址是否已验证是否在此级别上不提供任何安全性;客户实现仅具有UX目的。您将在后一部分中保护数据(保护数据)。

    4. 最后,您检查用户的角色是否设置为 admin 。这个角色自定义声称您将在本指南中稍后设置。如果用户的角色 admin ,则将其重定向到专用于Admins的React页面

  3. 在同一目录中,创建一个名为 auth-email-admin.html 的新文件,然后添加以下代码

    <!-- ... -->
    <body>
        <h1>Admin</h1>
    
        <script src="https://www.gstatic.com/firebasejs/8.0/firebase.js"></script>
        <script>
            var config = {
                apiKey: '<your apiKey>',
                authDomain: '<your authDomain>',
            };
            firebase.initializeApp(config);
        </script>
        <script>
            function checkRole() {
                firebase.auth().onAuthStateChanged(user => {
                    if (!user) {
                        window.location.href = 'auth-email.html';
                    } else {
                        // Print token to test the middlewares later on via HTTP client
                        /* firebase.auth().currentUser.getIdToken(true)
                        .then(token => console.log(token))
                        .catch(error => console.log(error)); */
    
                        user.getIdTokenResult(true)
                        .then(result => {
                            if (result.claims.role !== 'admin') {
                                window.location.href = 'auth-email.html';
                            }
                        })
                        .catch(error => console.log(error));
                    }
                });
            }
            window.onload = checkRole;
        </script>
    </body>
    <!-- ... -->
    

    firebase auth触发回调函数的时刻,您只需检查用户是否存在,以及角色索赔是否设置为 admin 。如果没有,您想将这样的访客重定向到 auth-email.html 页面,而不是显示管理员视图。这主要用于将访问者直接转到此页面而无需先验证。

在下一部分中,您会发现如何为用户分配角色。

分配角色

Blocking Functions

上图说明了您到目前为止所涵盖的部分。

您现在的重点是为用户分配自定义角色索赔,以便您可以控制谁可以访问哪些数据和服务。

显然,任务必须在安全环境(后端)中完成,以确保您只能管理逻辑。

实现此目的的方法是使用触发器。当某个事件发生在GIP中时,无服务器功能会触发并执行分配索赔的逻辑。

最初,在这种情况下仅有一个触发因素 - onCreate 。这仍然可用于基本的Firebase身份验证(遗产)。但是,该功能仅在创建新用户之后仅触发时限制。主要的含义是,您不能在访问者签署或登录之前执行自定义逻辑(例如,您无法限制某些访问者创建帐户)。

由于局限性,Google发布了阻止功能

添加阻止功能

后来,Google添加了两个新的触发器 - beforecreate beforesignin 。这些与无服务器云功能结合使用,可以在创建或签名之前执行任何自定义逻辑。他们称其为 Blocking Functions 根据您在功能主体中指定的条件中的帐户或签名。

Google可让您从两个不同的库中进行选择以实现相同的功能行为。一个可以在the GIP docs中找到,另一个在the Firebase docs中找到。唯一的区别是,Firebase库最近与其节点对应物一起引入了Python版本(当前在预览中),而GIP库当前仅支持节点实现。我选择了the GIP docs推荐的一个。

我不使用GCP中的内联编辑器出于多种原因(没有git,跟踪,â€),因此以下是我开发和部署 beforesignin 功能的步骤来自我的机器。

实施

  1. 运行npm init
  2. Run npm i gcip-cloud-functions
  3. 我更喜欢ES模块,所以我将"type": "module"添加到我的 package.json 配置

  4. 添加一个带有以下代码的新文件index.js

    import gcipCloudFunctions from 'gcip-cloud-functions';
    import { beforeSignInHandler } from './handlers.js';
    
    const adminEmails = process.env.ADMIN_EMAILS.split(',') || [];
    const authClient = new gcipCloudFunctions.Auth();
    
    export const beforeSignIn = authClient.functions()
      .beforeSignInHandler((user, context) => beforeSignInHandler(user, context, adminEmails));
    

    您可以使用环境变量指定管理电子邮件地址,该变量将传递给beforeSignInHandler函数。

  5. 添加一个带有以下代码的新文件handlers.js

    export const beforeSignInHandler = (user, context, adminEmails) => {
      const role = adminEmails.includes(user.email) ? 'admin' : 'user';
    
      console.log(`Signing in user: ${user.email} with role: ${role}`);
      return {
        customClaims: { role },
      }
    };
    

    您可以通过简单地从处理程序函数返回对象来修改用户数据。在上面的代码中,我们添加了一个新的自定义角色可以将其设置为 admin 用户

    我选择使用 beforesignin 触发而不是 beforecreate 触发触发来分配角色主张,因为我想具有更改的能力每个登录方式都可以访问用户。 GCP的自由层允许我在这里忽略资源,但请考虑您自己的用例。

See Full Code in GitHub Repo
还有更多文件,主要与单位测试有关。包含Babel软件包和 .babelrc 文件以启用使用ES模块的开玩笑测试。

部署

对于部署该功能,我建议使用 the gcloud CLI 并从终端或CI工具中执行命令。

  1. 运行gcloud config set project <your project id>设置正确的项目

  2. 以下是我用来部署功能的命令。更改案件的标志,尤其是ENV变量中的管理电子邮件(使用逗号分开)

    gcloud functions deploy beforeSignIn --runtime nodejs20 --trigger-http --allow-unauthenticated --region=europe-central2 --set-env-vars ADMIN_EMAILS='m@gmail.com'
    

为方便起见,我建议将命令添加到脚本 package.json 文件中。

设置触发器

部署函数后,必须使用GIP beforesignin 触发器将此功能连接起来。

  1. 转到GCP控制台(UI)的身份平台触发
  2. 连接您刚刚部署到 beforesignin 保存的功能

可选:限制注册

在撰写指南时,我意识到该函数的逻辑是如此短,以至于它甚至不反映阻止函数的有用阻止能力。因此,我决定通过添加此奖金白名单功能来展示行动中的阻塞。

现在,您可以选择任何东西作为白名单。 Google供应用户上下文对您的功能进行了参数,其中包括方便的数据(例如,用户的IP地址,提供商,电子邮件,电子邮件等)。下面的代码显示了白名单 基于电子邮件地址

  1. 创建一个新的处理程序

    import gcipCloudFunctions from 'gcip-cloud-functions';
    
    export const restrictRegistration = (user, context, whitelistEmails) => {
      console.log('New request to register user:', user.email);
      console.log('User Object:', user);
      console.log('Context Object:', context);
    
      if (!whitelistEmails.includes(user.email)) {
        console.log(`Unauthorized email "${user.email}"`);
        throw new gcipCloudFunctions.https.HttpsError(
          'permission-denied',
          `Unauthorized email "${user.email}"`
        );
      }
    
      console.log(`User with email: ${user.email} is authorized to register`);
    };
    
  2. index.js 中,导入处理程序并导出功能

    const whitelistEmails = process.env.WHITELIST_EMAILS.split(',') || [];
    
    export const beforeCreate = authClient.functions()
      .beforeCreateHandler((user, context) => restrictRegistration(user, context, whitelistEmails));
    
  3. 更改whitelist_emails env var并将函数部署为 beforecreate

    gcloud functions deploy beforeCreate --runtime nodejs20 --trigger-http --allow-unauthenticated --region=europe-central2 --set-env-vars WHITELIST_EMAILS='m@gmail.com,k@gmail.com'
    
  4. 在身份平台的设置中设置 beforecreate 触发

  5. 处理客户端客户端的错误

    // or try-catch
    .catch(err => {
        if (err.code !== 'auth/internal-error') {
            console.log('Unexpected error: ', err);
        } else {
            const regStatus = err.message.match(/"status":"(.*?)"/);
            const status = regStatus ? regStatus[1] : '';
    
            if (status === 'PERMISSION_DENIED') {
                            // handle the returned error (e.g. display message)
            } else {
                    console.log('Unhandled blocking error: ', err);
            }
        }
    });
    

测试客户流

Client using Blocking Functions

该图说明了您实现的当前状态。

此时,使用管理员电子邮件地址从客户端签署时,您会重定向到 Admin 页面。签署使用非ADMIN电子邮件地址将触发重定向。

dote
任何人都可以修改您的客户端代码,因此客户端重定向不提供任何安全性;客户实现仅用于UX目的。您将在下一节中保护数据(保护数据)。

如果您遇到不同的行为,请调试客户端并检查GCP中的云功能日志,或在x.com上滑到我的DMS(Twitter)。

保护数据

您在上一节中看到的图中包含一个灰色的部分。这是您接下来关注的部分,目的是保护您的数据免受未经授权的用户的影响。

您的用户在客户端上登录一旦登录,响应中包含的(JWT)令牌将在本地存储在其浏览器中。每当您向后端提出请求以验证其有效性并提取用户数据时,都必须发送令牌。

为了使您的生活更轻松,每当您使用Firestore,Realtime DB或存储等燃料服务时,将在每个请求中自动发送令牌。如果您使用其他任何内容,则必须将令牌包含在请求中。

以下各节将涵盖这两种情况,但如果您不计划使用任何firebase产品,请随时直接跳到middlewares

控制Firebase的访问

在这种情况下,当发送令牌到达时,它会自动被Firebase解码,而无需您做任何事情。然后,请求继续前往Firebase Rules,您指定必须满足的条件才能访问数据。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function verifiedAdmin() {
        return request.auth != null && request.auth.token.email_verified && request.auth.token.role == "admin";
    }

    match /admin/{document=**} {
        // only allow to read the admin content if claim "role" is set "admin"
      allow read: if verifiedAdmin();
      allow write: if false;
    }
    match /{document=**} {
      allow read: if false;
      allow write: if false;
    }
  }
}

上面的firebase规则示例显示了如何在用户是经过验证的管理员(请参阅verifiedAdmin()函数)的情况下访问 admin 集合中的数据。在规则中,可以根据request.auth.token访问自定义索赔。

email_verified的状况仅适用于untrusted providers,但我建议您保留条件作为预防措施。

使用Middlewares控制访问

在这种情况下,您将使用身份验证( auth )和授权( authz )中间验证来保护REST API中的路线处理程序。请参阅下图中的第7步到#12的步骤。

Middlewares

首先,我将首先引导您完成Middlewares的实现,然后,最后,我将向您展示如何使用真实(JWT)代币测试其功能。

我创建了两个在逻辑上相同的实现,一个在GO(v1.20)中,另一个在节点(v20)中。是的,版本非常新鲜,您欢迎您。

两个实现的完整代码(包括单元测试,docker文件等)都在the repo中可用。

在本指南中,我将仅向您展示相关的代码部分,但是最后,我将解释如何运行和部署此类服务。

选择您的后端
Go Middlewares
Node Middlewares

去中间

设置

为了使我的生活更轻松,我添加了一个流行的轻量级框架Fiber。无论您使用哪种软件包,该过程和大多数代码都将保持不变。

接下来,您将需要获取Firebase Admin SDK -go get firebase.google.com/go

firebase admin sdk
您会随时随地使用此SDK 后端与Google Identity平台进行通信。同样,在这种情况下,Google决定重复使用已经开发的Firebase库,而不是专门为身份平台制作新的Firebase库。

实施

  1. 让SDK通过设置 firebase应用程序知道您正在使用的项目。然后初始化 auth Client

    package config
    
    import (
        "context"
        "os"
        "log"
    
        firebase "firebase.google.com/go"
        "firebase.google.com/go/auth"
    )
    
    func InitFirebase() *firebase.App {
        conf := &firebase.Config{ProjectID: os.Getenv("GCP_PROJECT_ID")}
        app, err := firebase.NewApp(context.Background(), conf)
        if err != nil {
            log.Printf("error initializing app: %v\n", err)
            return nil
        }
    
        return app
    }
    
    func InitFirebaseAuth(app *firebase.App) *auth.Client {
        client, err := app.Auth(context.Background())
        if err != nil {
            log.Printf("error initializing firebase auth client: %v\n", err)
            return nil
        }
    
        return client
    }
    

    在某些情况下,您可能不得不指定 projectId ,但我强烈建议您避免进行项目间事故。

  2. 接下来,添加 auth 中间件,如下所示

    package middlewares
    
    import (
        "log"
        "strings"
        "context"
    
        m "middleware-go/src/models"
    
        "github.com/gofiber/fiber/v2"
        "firebase.google.com/go/auth"
    )
    
    type AuthClient interface {
        VerifyIDToken(ctx context.Context, idToken string) (*auth.Token, error)
    }
    
    func UseAuth(authClient AuthClient) fiber.Handler {
        return func(c *fiber.Ctx) error {
            log.Println("In Auth middleware")
    
            authHeader := c.Get("Authorization")
            if authHeader == "" {
                log.Println("Missing Authorization header")
                return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
                    "message": "Missing Authorization header",
                })
            }
    
            // Bearer token split to get the token without "Bearer" in front
            val := strings.Split(authHeader, " ")
            if len(val) != 2 {
                log.Println("Invalid Authorization header")
                return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
                    "message": "Invalid Authorization header",
                })
            }
            token := val[1]
    
            decodedToken, err := authClient.VerifyIDToken(c.Context(), token)
            if err != nil {
                log.Printf("Error verifying token. Error: %v\n", err)
                return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                    "message": "Invalid authentication token",
                })
            }
    
            // Only needed for untrusted providers (e.g. email/password)
            // But I suggest verifying it for all providers
            if decodedToken.Claims["email_verified"] != true {
                log.Println("Email not verified")
                return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                    "message": "Email not verified",
                })
            }
    
            if _, ok := decodedToken.Claims["role"]; !ok {
                log.Println("Role not present in token")
                return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                    "message": "Role not present in token",
                })
            }
    
            user := &m.User{
                UID: decodedToken.UID,
                Email: decodedToken.Claims["email"].(string),
                Role: decodedToken.Claims["role"].(string),
            }
            c.Locals("user", user)
    
            log.Println("Successfully authenticated")
            log.Printf("Email: %v\n", user.Email)
            log.Printf("Role: %v\n", user.Role)
    
            return c.Next()
        }
    }
    
    1. 从授权标题中提取令牌
    2. 将令牌传递给authClient.VerifyIDToken()接口方法进行验证( authclient 很快将被Firebase的Auth Client替换)
    3. 用户数据,包括角色主张,存储在当地人中用于使用下一个中间件或处理程序
  3. 添加 authz (授权)中间件

    package middlewares
    
    import (
        "log"
    
        m "middleware-go/src/models"
    
        "github.com/gofiber/fiber/v2"
    )
    
    func UseAuthZ(requiredRole string) fiber.Handler {
        return func(c *fiber.Ctx) error {
            log.Println("In AuthZ middleware")
    
            user, ok := c.Locals("user").(*m.User)
            if !ok {
                log.Println("User not found in context")
                return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                    "message": "Unauthorized",
                })
            }
    
            if user.Role == "" {
                log.Println("User role not set")
                return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                    "message": "Unauthorized",
                })
            }
    
            if user.Role != requiredRole {
                log.Printf("User with email %s and role %s tried to access " +
                    "a route that was for the %s role only",
                    user.Email, user.Role, requiredRole)
                return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
                    "message": "Forbidden",
                })
            }
    
            log.Printf("User with email %s and role %s authorized",
                user.Email, user.Role)
    
            return c.Next()
        }
    }
    

    if user.Role != requiredRole实现非常原始。您目前要定义逻辑授权规则(例如,您可能需要允许 admin 访问任何资源)。

  4. 最后,我们必须将流放在main.go

    package main
    
    import (
        "os"
        "log"
    
        c "middleware-go/src/config"
        mw "middleware-go/src/middlewares"
        m "middleware-go/src/models"
    
        "github.com/gofiber/fiber/v2"
    )
    
    func userHandler(c *fiber.Ctx) error {
        user := c.Locals("user").(*m.User)
        return c.JSON(user)
    }
    
    func main() {
        app := fiber.New()
    
        firebaseApp := c.InitFirebase()
        authClient := c.InitFirebaseAuth(firebaseApp)
    
        // only authenticated admins can access this route
        app.Get("/user", mw.UseAuth(authClient), mw.UseAuthZ("admin"), userHandler)
    
        log.Fatal(app.Listen(":" + os.Getenv("PORT")))
    }
    
    1. 在此级别初始化firebase auth客户端
    2. 将客户传递到 auth 中间件
    3. 指定您想要 authz 中间件以允许此路线
    4. 如果整个流通过(用户是经过验证的管理员),请执行处理程序逻辑

在本地运行

上帝保佑Docker,因为使用docker-compose,您简单地:

  1. pull the repo
  2. cd middleware-go
  3. 更改 docker-compose.yml 文件中的环境变量

    google_application_credentials 必须映射到容器中。有一个很好的Google docs page解释了如何获取文件。我使用the default credentials避免麻烦。

  4. 运行docker-compose up,该服务应启动并运行,准备接收请求

是2023年;我不会解释任何其他运行的方式。

用令牌测试

  1. 要获取令牌,转到您的客户端代码并搜索注释的行 在React/下一个客户端 app/page.js 在Vanilla JS 中,两个文件
  2. 取消点击线并登录(尝试 - admin和非Admin帐户)
  3. 复制浏览器控制台印刷的令牌
  4. 打开http客户端(curl,postman,insomnia,â€)
  5. SET 授权带有值的标题承载者
  6. 将get请求发送到 http://localhost:8089/user
  7. 使用 Admin 令牌进行测试时,您应该在响应中查看用户数据

部署

最快的方法是部署到Cloud Run。该服务将使用 dockerfile 来构建生产图像。您甚至可以省略 google_application_credentials env var,因为默认情况下是在GCP项目中。

s

节点中间

设置

安装Express和Firebase Admin SDK(npm i firebase-admin)。

firebase admin sdk
您会随时随地使用此SDK 后端与Google Identity平台进行通信。同样,在这种情况下,Google决定重复使用已经开发的Firebase库,而不是专门为身份平台制作新的Firebase库。

实施

  1. 让SDK通过设置 firebase应用程序知道您正在使用的项目。然后导出 auth Client

    import admin from 'firebase-admin';
    
    admin.initializeApp({
      credential: admin.credential.applicationDefault(),
      projectId: process.env.GCP_PROJECT_ID,
    });
    
    const authClient = admin.auth();
    
    export { authClient };
    

    在某些情况下,您可能不得不指定 projectId ,但我强烈建议您避免进行项目间事故。

  2. 接下来,添加 auth 中间件,如下所示

    function useAuth(authClient) {
      return async function(req, res, next) {
        console.log('In Auth middleware');
    
        const authHeader = req.headers.authorization;
        if (!authHeader) {
          console.log('Missing Authorization header');
          return res.status(400).json({ message: 'Missing Authorization header' });
        }
    
        const [bearer, token] = authHeader.split(' ');
        if (bearer !== 'Bearer' || !token) {
          console.log('Invalid Authorization header');
          return res.status(400).json({ message: 'Invalid Authorization header' });
        }
    
        let decodedToken;
        try {
          decodedToken = await authClient.verifyIdToken(token);
        } catch (error) {
          console.log(`Error verifying token. Error: ${error}`);
          return res.status(401).json({ message: 'Invalid authentication token' });
        }
    
        // Only needed for untrusted providers (e.g. email/password)
        // But I suggest verifying it for all providers
        if (!decodedToken.email_verified) {
          console.log('Email not verified');
          return res.status(401).json({ message: 'Email not verified' });
        }
    
        if (!decodedToken.role) {
          console.log('Role not present in token');
          return res.status(401).json({ message: 'Role not present in token' });
        }
    
        const user = {
          uid: decodedToken.uid,
          email: decodedToken.email,
          role: decodedToken.role,
        };
        res.locals.user = user;
    
        console.log('Successfully authenticated');
        console.log(`Email: ${user.email}`);
        console.log(`Role: ${user.role}`);
    
        next();
      };
    }
    
    export { useAuth };
    
    1. 从授权标题中提取令牌
    2. 将令牌传递到authClient.verifyIdToken()函数进行验证
    3. 解码的用户数据,包括角色主张,存储在当地人中用于使用
    4. 的下一个中间件或处理程序
  3. 添加 authz (授权)中间件

    function useAuthZ(requiredRole) {
      return function(req, res, next) {
        console.log('In AuthZ middleware');
    
        const user = res.locals.user;
        if (!user) {
          console.log('User not found in context');
          return res.status(401).json({ message: 'Unauthorized' });
        }
    
        if (!user.role) {
          console.log('User role not set');
          return res.status(401).json({ message: 'Unauthorized' });
        }
    
        if (user.role !== requiredRole) {
          console.log(`User with email ${user.email} and role ${user.role} tried to access a route that was for the ${requiredRole} role only`);
          return res.status(403).json({ message: 'Forbidden' });
        }
    
        console.log(`User with email ${user.email} and role ${user.role} authorized`);
    
        next();
      };
    }
    
    export { useAuthZ };
    

    if (user.role !== requiredRole)实现非常原始。您目前要定义逻辑授权规则(例如,您可能需要允许 admin 访问任何资源)。

  4. 最后,我们必须将流放在index.js

    import express from 'express';
    import { authClient } from './firebase.js';
    import { useAuth } from './auth.js';
    import { useAuthZ } from './authz.js';
    
    const app = express();
    
    app.get('/user', useAuth(authClient), useAuthZ('admin'), (req, res) => {
      const user = res.locals.user;
      res.json(user);
    });
    
    const port = process.env.PORT;
    app.listen(port, () => {
      console.log(`Server is running on port ${port}`);
    });
    
    1. 将客户传递到 auth 中间件
    2. 指定您想要 authz 中间件以允许此路线
    3. 如果整个流通过(用户是经过验证的管理员),请执行处理程序逻辑

在本地运行

上帝保佑Docker,因为使用docker-compose,您简单地:

  1. pull the repo
  2. cd middleware-node
  3. 更改 docker-compose.yml 文件中的环境变量

    google_application_credentials 必须映射到容器中。有一个很好的Google docs page解释了如何获取文件。我使用the default credentials避免麻烦。

  4. 运行docker-compose up,该服务应启动并运行,准备接收请求

是2023年;我不会解释任何其他运行的方式。

用令牌测试

  1. 要获取令牌,转到您的客户端代码并搜索注释的行 在React/下一个客户端 app/page.js 在Vanilla JS 中,两个文件
  2. 取消点击线并登录(尝试 - admin和非Admin帐户)
  3. 复制浏览器控制台印刷的令牌
  4. 使用HTTP客户端(curl,Postman,Insomnia,â€)
  5. SET 授权带有值的标题承载者
  6. 将get请求发送到 http://localhost:8088/user
  7. 使用 admin 令牌进行测试时,您会在响应中查看用户数据

部署

最快的方法是部署到Cloud Run。该服务将使用 dockerfile 来构建生产图像。您甚至可以省略 google_application_credentials env var,因为默认情况下,它们在GCP项目中。

包起来

Full Flow Diagram

现在,您已经有了Google建议使用Identity平台来控制访问时的全面概述。

我的2023年计划是在公共场所建立项目并撰写文章。如果您想查看我的下一个动作,请在x.com(Twitter)上关注我。