首页 > 解决方案 > Wear OS 上 Firebase 授权的最佳实践

问题描述

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

谢谢你,唐尼

标签: androidfirebasewear-osandroid-wear-data-apiandroid-wear-2.0

解决方案


该文档涵盖了您可以使用的不同身份验证方法。

最终,您至少需要一种基于 Web 的方法来对手表进行身份验证,因为您无法保证用户会安装您的配套应用程序或手表未连接到 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. [云功能] 解析上一步的允许/拒绝请求并为该用户生成自定义身份验证令牌
  4. [Cloud Function] 将自定义身份验证令牌交换为 Firebase ID 令牌并 https://wear.googleapis.com/3p_auth/com.your.package.name使用 GET 参数accessTokenrefreshToken.
  5. [观看] 解析响应

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


推荐阅读