如何跨子域共享 Firebase 身份验证
这篇文章适用于已经熟悉 Firebase、Firebase 身份验证以及在其 Web 应用中使用 Firebase 的人们。
如果您在 Web 应用中使用 Firebase 身份验证,可能会遇到 Firebase 仅支持单域名身份验证的问题。这意味着,如果您的应用体验分布在多个子域名,则用户必须分别登录每个子域名。更糟糕的是,用户还必须分别退出每个子域名。
如果您的应用程序在子域名之间共享品牌标识,则可能会带来安全风险。用户期望退出时app1.domain.com
也同时退出,这或许是合理的app2.domain.com
。许多流行的应用程序在子域名之间共享登录状态,例如 Google 本身。
我花了比预期更长的时间来实现跨子域的单点登录,我写这篇文章是希望下一个人能更轻松地做到这一点。
从高层次来看,这是我们的设置:
- 我们在不同的领域有三个应用程序。
accounts.domain.com
app1.domain.com
app2.domain.com
- 我们有三个 Firebase 函数
...cloudfunctions.net/users-signin
...cloudfunctions.net/users-checkAuthStatus
...cloudfunctions.net/users-signout
登录方式:
- 有人导航到该
accounts.domain.com
应用程序 - 他们提供他们的身份验证信息
- 该身份验证信息被发送到我们的
/users-signin
云功能,该功能会验证信息,如果有效,则设置__session
包含用户 UID 的签名 cookie,并向客户端返回成功指示。 - 成功后,客户端将调用
/users-checkAuthStatus
云函数来查找签名的__session
cookie、提取用户 UID,并使用 UID 和firebase-admin
SDK 来创建自定义身份验证令牌并将其返回给客户端。 - 当客户端收到此自定义身份验证令牌时,它会使用它通过 firebase javascript SDK 登录。
当用户导航到其他应用(例如)时,app1.domain.com
该应用会首先检查用户是否已通过 Firebase Auth 登录。如果没有,它会调用/users-checkAuthStatus
云函数,该函数会查找已签名的__session
Cookie,并在适用的情况下向客户端返回自定义身份验证令牌。然后,客户端会使用自定义身份验证令牌(如果有)为用户登录。
如果用户app1.domain.com
尚未登录但想要登录,您可以将其发送到,然后在登录完成后将accounts.domain.com
其重定向回。app1.domain.com
为了退出登录,客户端会通过调用 来清除本地身份验证状态signOut()
,firebase-js-sdk
并调用...cloudfunctions.net/users-signout
来清除__session
Cookie。此外,客户端需要通知所有其他连接的客户端用户已退出登录,以便他们可以signOut()
使用 firebase-js-sdk 进行调用。
真正让事情安全运转起来。
这是高层次的概述,但为了真正让它发挥作用,我们需要处理一些事情,比如跨站点脚本、cookie、处理提供商身份验证等。
登录
首先,您需要决定如何在服务器上验证身份验证。
一种可能性是,正常地在客户端上对某人进行身份验证accounts.domain.com
(使用 Firebase Auth),然后将他们的 idToken 发送到服务器,在服务器上使用管理 SDK 验证 ID 令牌,验证issuedAtTime
与 ID 令牌关联的内容(例如,确保它是在过去 5 分钟内创建的),并验证与 ID 令牌关联的提供程序(例如,确保它不是使用自定义身份验证令牌创建的)。
另一种可能性是,如果有人通过 Facebook 或 Twitter 等提供商进行身份验证,则使用该提供商的 SDK 对其进行身份验证,检索authToken
,然后将发送authToken
到服务器,然后按照提供商的说明在服务器上验证令牌。
无论如何将凭据传递给服务器,如果服务器确定身份验证有效,则需要设置一个__session
cookie,该 cookie 等于与用户关联的 Firebase 身份验证 UID,以及设置一个跨站点脚本 cookie,我们将使用它来防止跨站点脚本攻击。
Cookie__session
应该是signed
、secure
(表示仅 HTTPS)和httponly
(表示 JavaScript 无法访问)。跨站脚本 Cookie(我们称之为 )csst
应该是 ,secure
但不能是signed
或httponly
。相反,csst
应该使用对jsonwebtoken
令牌进行签名并记录令牌主题(即 auth UID)的库来创建 Cookie。与这两个 Cookie 关联的域应该是您应用程序的根域(即domain.com
)。这确保 Cookie 在子域之间共享。
浏览器不允许您为其他域设置 Cookie。这是一个问题,因为默认情况下,您的 Firebase 函数与您的应用位于不同的域中。您可以按照这些说明为您的函数使用 Firebase Hosting 和自定义域。另请注意,为您的函数使用 Firebase Hosting 意味着您只能__session
在服务器端读取 Cookie(这不会影响我们对csst
令牌的使用)。即使您将 Firebase 函数设置为使用您的自定义域,您在开发过程中仍可能会遇到问题:在我最初的设置中,我在本地托管我的应用,但将 Firebase 函数部署到一个特殊的开发 Firebase 项目中。这意味着与函数关联的域不是 localhost(意味着函数无法设置客户端可以看到的 Cookie)。
我想出了两种解决此情况的方法。
- 在开发过程中禁用跨站点脚本检查,并
domain
从__session
Cookie 中移除相关规范。此方法有效,因为__session
Cookie 无论如何都只能由 Firebase Functions 读取,因此即使__session
Cookie 不在子域之间共享也没问题(在这种情况下,与 Cookie 关联的域__session
将是您的 Firebase Functions 域)。 - 在开发过程中,在本地提供你的功能。Firebase在文档中提供了如何使用其本地模拟器的说明。
- 本地函数模拟器现在可与 Node 8+ 配合使用
本地模拟器的一个问题是它只适用于 nodejs 6(仅供参考,我发现当前版本的 expressjs 在 nodejs6 中不起作用)。 - 这不再是必要的,因为本地函数模拟器现在可以与 node 8+ 一起使用
另一种选择是构建自己的 Express 应用来托管开发过程中的功能。这是路线……
- 本地函数模拟器现在可与 Node 8+ 配合使用
要设置 Cookie,您需要使用onRequest
Firebase Function而不是onCall
Firebase Function。您还需要处理CORS以及所有相关功能。另外,我还要指出的是,Google Chrome 浏览器有一个非常出乎意料的怪癖:如果响应头来自不同的域名,它会从响应中删除该头。我讨厌这个怪癖。我花了好几个小时才知道 Cookie 没有被设置,但实际上它被设置了。更多信息请参阅此 SO 问题。另外,执行 CORS 请求时,您需要指定请求“带有凭据”才能发送 Cookie。服务器还需要指定 CORS 请求允许使用凭据,以便接收凭据。set-cookie
set-cookie
检查授权状态
无论如何,在客户端设置完__session
cookie 和cookie 之后,客户端就可以调用端点了。调用时,客户端需要找到cookie,提取其令牌,并将令牌设置在请求的标头中。端点收到请求后,会提取授权标头中包含的令牌,验证令牌上的签名,并确保令牌的主题与签名 cookie 中包含的身份验证 UID 匹配。假设一切正常,您可以使用SDK 生成自定义身份验证令牌并将其发送给客户端。如果无效,请清除客户端上所有旧的 cookie 。csst
/users-checkAuthStatus
csst
Authorization: Bearer ${token}
checkAuthStatus
csst
__session
firebase-admin
__session
csst
最后,当客户端收到这些自定义身份验证令牌之一时,请确保客户端使用SESSION
身份验证持久性进行登录。这意味着身份验证状态将在页面刷新后保留,但当与域关联的每个标签页关闭时,身份验证状态将被清除。每当应用初始化时,您都需要执行以下操作:
- 检查此人是否已登录。
- 如果没有,则调用
/users-checkAuthStatus
端点,如果您收到自定义身份验证令牌作为响应,则使用它来登录用户。- 如果您没有收到任何信息,则表明该用户尚未登录。
退出
当有人退出时,客户端需要调用/users-signout
端点,该端点将清除客户端上的所有__session
/ csst
cookies,并调用signOut()
firebase-js-sdk 的方法。此外,您还需要以某种方式 ping 任何其他打开的应用,以告知它们退出操作——此时,它们应该调用signOut()
firebase-js-sdk 的方法退出。提醒一下,如果某个应用已关闭,firebase-js-sdk 的身份验证状态已清除。
为了ping通其他打开的应用,告知它们退出firebase-js-sdk,我发现最简单的方法是监控csst
cookie的存在。如果csst
cookie消失,则表明该用户已退出,您的应用应该调用signOut()
firebase sdk的方法。
总结
好了,我的概述到此结束。让 Firebase 身份验证跨子域工作并非易事,但无需太多工作即可实现。遗憾的是,您需要熟悉一些概念,例如 CORS、Cookie、JWT、Firebase 身份验证本身等等。
祝你好运!
编辑(2021年5月14日)
-
有几个人要求提供可运行设置的示例(也就是代码)。我当然理解为什么这会很有帮助,但我没有计划这么做(也就是说,我不愿意花时间)。如果有人读到这篇文章,整理了一个示例代码库并在评论区留言给我,我会在这篇文章中更新你的代码库链接(并注明出处),以便其他人也能从中受益。
-
另一位开发人员提出了这种方法的变体(他们认为这是一种改进),您可以在这里阅读,跨域 Firebase 身份验证:一种简单的方法。我根本没有测试过这种方法,所以我分享它但不认可它(但有选择总是好的,对吧?)。
-
无关地,我最近发现你可以(相当容易地)为 Firebase Firestore 实现一个简单的查询缓存,这可以提高性能并可能降低成本。你可以在这里看到概述: