Wear OS 上 Firebase 授权的最佳实践

Posted

技术标签:

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

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

谢谢你, 唐尼

【问题讨论】:

【参考方案1】:

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

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

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

选项 1:短期令牌交换

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

    提示用户打开登录网页或打开配套应用(或发送RemoteIntent 为他们打开) 通过身份验证后,调用创建身份验证代码(大约 5-6 个字母数字字符长)的云函数,并将其安全地存储在您选择的数据库中,有效期为 1 到 2 分钟。 让用户直接在手表上输入代码(或使用数据层将其发送到手表)。 将代码发送到另一个 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

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

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

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

【讨论】:

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

以上是关于Wear OS 上 Firebase 授权的最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

Wear OS 上 Jetpack Compose 中的 BasicTextField 问题

Android Wear OS 关闭 Wifi

Wear OS 更新一览 | 2021 Android 开发者峰会

通过蓝牙从 Wear OS 发送 UDP 消息

欢迎体验 | Wear OS 版 Compose 开发者预览版

从 Wear 应用程序启动 OS 的连接屏幕