在 Wear OS 上进行 Firebase 授权的最佳实践

时间:2021-07-19 07:17:59

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

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

谢谢, 唐尼

1 个答案:

答案 0 :(得分:0)

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

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

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

选项 1:短期代币交换

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

  1. 提示用户打开登录网页或打开配套应用(或发送 RemoteIntent 为他们打开)
  2. 通过身份验证后,调用创建身份验证代码(长度约为 5-6 个字母数字字符)的 Cloud Function,并将其安全地存储在您选择的数据库中,有效期为 1 到 2 分钟。
  3. 让用户直接在手表上输入代码(或使用数据层将其发送到手表)。
  4. 将代码发送到另一个 Cloud Functions 函数以将其交换为 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 参数 https://wear.googleapis.com/3p_auth/com.your.package.nameaccessToken 重定向到 refreshToken
  5. [观看] 解析响应

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

相关问题