【问题标题】:Best practice for firebase authorization on Wear OSWear OS 上 Firebase 授权的最佳实践
【发布时间】:2021-09-26 21:11:41
【问题描述】:

我正在 Wear OS 上为连接到 Android 设备的随附应用程序实现一个 firebase 实时数据库,我想知道在佩戴手表上验证用户身份的最佳做法是什么。在小屏幕上输入电子邮件和密码不是很方便。是否可以通过 Wear os 数据层传递 Firebase 授权令牌,如果可以,您将如何使用来自 Android 设备的令牌在 Wear 手表上对用户进行身份验证?

谢谢你, 唐尼

【问题讨论】:

    标签: android firebase wear-os android-wear-data-api android-wear-2.0


    【解决方案1】:

    文档涵盖了您可以使用的different authentication approaches。

    最终,您至少需要一种基于网络的方法来验证手表,因为您无法保证用户会安装您的配套应用程序或手表未连接到 iOS 设备。

    您有两种可用的方法(我能想到):

    选项 1:短期令牌交换

    在此方法中,您执行以下步骤:

    1. 提示用户打开登录网页或打开配套应用(或发送RemoteIntent 为他们打开)
    2. 通过身份验证后,调用创建身份验证代码(大约 5-6 个字母数字字符长)的云函数,并将其安全地存储在您选择的数据库中,有效期为 1 到 2 分钟。
    3. 让用户直接在手表上输入代码(或使用数据层将其发送到手表)。
    4. 将代码发送到另一个 Cloud Function 以将其交换为 Firebase ID 令牌。
    const functions = require('firebase-functions');
    
    const sha256 = (s) => require('crypto').createHash('sha256').update(s).digest('base64');
    
    const lazyFirebaseAdmin = () => {
      const admin = require('firebase-admin');
      try {
        admin.app();
      } catch {
        admin.initializeApp();
      }
      return admin;
    }
    
    const createUserAuthCode = async (uid) => {
      const chars = "0123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"; // omitted O, I, l
      let code = "", charsLen = chars.length;
      for (let i=0; i<6; i++)
        code += chars[Math.floor(Math.random() * charsLen)];
    
      const encoded = sha256(code);
    
      await lazyFirebaseAdmin()
        .firestore()
        .collection('_server/auth/userCodes')
        .doc(encoded)
        .create({
          created: admin.firestore.FieldValue.serverTimestamp(),
          uid
        });
    
      return code;
    }
    
    const validateUserAuthCode = async (code) => {
      const encoded = sha256(code);
    
      const codeRef = lazyFirebaseAdmin()
        .firestore()
        .collection('_server/auth/userCodes')
        .doc(encoded);
    
      const snapshot = await codeRef.get();
    
      if (!snapshot.exists)
        return null; // not found
    
      const { uid, created } = snapshot.data();
    
      await codeRef.delete();
    
      if (created.toMillis() < Date.now() - (2 * 60 * 1000)) {
        return null; // too old
      }
    
      return uid || null;
    }
    
    const getDeviceCode = functions.https.onCall(async (data, context) => {
      if (context.app === undefined) { // If you want to use Firebase App Check to mitigate abuse
        throw new HttpsError(
          'failed-precondition',
          'Unrecognized caller');
      }
    
      if (!context.auth) {
        throw new HttpsError(
          'failed-precondition',
          'You must be authenticated to request a device code');
      }
    
      try {
        return {
          code: await createUserAuthCode(context.auth.uid)
        };
      } catch (error) {
        throw new HttpsError(
          'unknown',
          'Couldn\'t generate device code',
          { message: error.code || error.message }
        );
      }
    });
    
    const exchangeDeviceCode = functions.https.onRequest(async (req, res) => {
      if (req.method !== "GET") {
        console.log("Rejected unexpected " + req.method + " request");
        res.status(405)
          .set("Allow", "GET")
          .end();
        return;
      }
    
      const code = req.query.code;
    
      if (typeof code !== "string") {
        res.status(400)
          .json({ message: "Missing code param" });
        return;
      }
    
      try {
        const uid = await validateUserAuthCode(code);
    
        const token = await admin.auth()
          .createCustomToken(uid, {
            isDeviceToken: true // by having this, you can prevent the watch
                                // auth tokens from doing privileged actions
          });
    
        const response = await fetch({
          url: "https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=[API_KEY]", // TODO: Replace with Web API key
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ token, returnSecureToken: true })
        });
    
        // idToken - Firebase ID token (access token)
        // refreshToken - refresh token for this device authentication token
        // expiresIn - number of seconds to ID token expiry
    
        res
          .status(response.status)
          .set("Content-Type", "application/json")
          .send(response.text());
      } catch (err) {
        res.status(500)
          .json({ error: "Encountered unexpected error" });
      }
    });
    

    在客户端,您可以使用以下任一方式调用第一个函数(登录后):

    // Java
    var getDeviceCodeFunc = FirebaseFunctions.getInstance().getHttpsCallable("getDeviceCode")
    
    getDeviceCodeFunc.call()
      .addOnCompleteListener({ task ->
        if (task.isSuccessful()) {
          // got code!
        } else {
          // failed!
        }
      });
    
    // Web/JavaScript
    const getDeviceCode = firebase.functions().httpsCallable("getDeviceCode");
    const code = await getDeviceCode();
    

    然后,一旦用户输入代码,将其发送到

    GET https://us-central1-[PROJECT_ID].cloudfunctions.net/exchangeDeviceCode?code=[TYPED_CODE]
    

    选项2:PKCE

    在此方法中,您执行以下步骤:

    1. [观看]启动sendAuthorizationRequest()流
    2. [网页] 验证用户身份(如果需要)并请求连接设备的权限
    3. [Cloud Function] 解析上一步的允许/拒绝请求并为该用户生成自定义身份验证令牌
    4. [Cloud Function] 将自定义身份验证令牌交换为 Firebase ID 令牌并使用 GET 参数 accessToken 和 refreshToken 重定向到 https://wear.googleapis.com/3p_auth/com.your.package.name。
    5. [观看]解析响应

    注意:这对于您正在尝试做的事情可能有点过头了。但是,如果您真的不希望有人在手表上输入代码,则可以选择使用它。您可以使用 oauth2-server 来代理发布 Firebase ID 令牌(访问令牌)。

    【讨论】:

    • 感谢您的详细回复。我阅读了您所指的文档,但不确定如何实施。你的解释帮助我解决了所有的问题。只是一个后续问题。所以不可能使用数据层将firebase令牌id发送到手表(假设用户在android设备上拥有应用程序)?
    • @donnyrewq 发送 ID 令牌有点没有意义,因为它只会持续一个小时,这意味着需要至少每小时重新连接到手机。使用此处描述的方法,您可以生成可供手表使用的新访问令牌和刷新令牌对,因此它不需要一直与您的应用程序对话。
    • 有道理,感谢您为我澄清。
    猜你喜欢
    • 2011-08-06
    • 2020-10-02
    • 1970-01-01
    • 2021-07-13
    • 2021-05-26
    • 1970-01-01
    • 2023-02-11
    • 2017-02-04
    • 1970-01-01
    相关资源
    最近更新 更多