【问题标题】:Cross domain state cookie issue for oAuth using firebase functions while on the same domain在同一域上使用 firebase 函数的 oAuth 的跨域状态 cookie 问题
【发布时间】:2019-12-06 18:42:12
【问题描述】:

我正在为 firebase 平台的用户实现 oAuth 登录。

一切正常,除非用户 禁用跨域cookie。

这就是我所做的。

  1. 从我的域/应用程序中,用户被重定向到云功能。
  2. can 函数设置 state cookie 并将用户重定向到 oAuth 提供程序。
  3. 用户登录到 oAuth 提供程序并被重定向回另一个函数以获取代码等。这就是问题

如果用户在浏览器中禁用了跨域方 cookie,则在上面的第 3 步中,该函数无法读取任何 cookie。 这两个函数都在同一个域中,如下面的屏幕截图所示。

有什么办法可以解决这个问题吗?我的方法做错了吗?

我不明白为什么这两个函数被视为跨域。

更新以包含更多信息

请求:

Request URL: https://europe-west2-quantified-self-io.cloudfunctions.net/authRedirect
Request Method: GET
Status Code: 302 
Remote Address: [2a00:1450:4007:811::200e]:443
Referrer Policy: no-referrer-when-downgrade

请求标头

:authority: europe-west2-quantified-self-io.cloudfunctions.net
:method: GET
:path: /authRedirect
:scheme: https
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
accept-encoding: gzip, deflate, br
accept-language: en-GB,en-US;q=0.9,en;q=0.8
cookie: signInWithService=false; state=877798d3672e7d6fa9588b03f1e26794f4ede3a0
dnt: 1
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36

响应标头

alt-svc: quic=":443"; ma=2592000; v="46,43,39"
cache-control: private
content-encoding: gzip
content-length: 218
content-type: text/html; charset=utf-8
date: Sat, 03 Aug 2019 08:55:18 GMT
function-execution-id: c8rjc7xnvoy8
location: https://cloudapi-oauth.suunto.com/oauth/authorize?response_type=code&client_id=xxx&redirect_uri=&scope=workout&state=1c8073866d1ffaacf2d4709090ad099872718afa
server: Google Frontend
set-cookie: state=1c8073866d1ffaacf2d4709090ad099872718afa; Max-Age=3600; Path=/; Expires=Sat, 03 Aug 2019 09:55:18 GMT; HttpOnly; Secure
set-cookie: signInWithService=false; Max-Age=3600; Path=/; Expires=Sat, 03 Aug 2019 09:55:18 GMT; HttpOnly; Secure
status: 302
vary: Accept
x-cloud-trace-context: 99a93680a17770f848f200a9e729b122;o=1
x-powered-by: Express

之后,一旦用户从服务返回,他对解析 cookie 的代码(或处理它的函数)进行了身份验证:

export const authToken = functions.region('europe-west2').https.onRequest(async (req, res) => {
  const oauth2 = suuntoAppAuth();
  cookieParser()(req, res, async () => {
    try {
      const currentDate = new Date();
      const signInWithService = req.cookies.signInWithService === 'true';
      console.log('Should sign in:', signInWithService);
      console.log('Received verification state:', req.cookies.state);
      console.log('Received state:', req.query.state);
      if (!req.cookies.state) {
        throw new Error('State cookie not set or expired. Maybe you took too long to authorize. Please try again.');
      } else if (req.cookies.state !== req.query.state) {
        throw new Error('State validation failed');
      }
      console.log('Received auth code:', req.query.code);
      const results = await oauth2.authorizationCode.getToken({
        code: req.query.code,
        redirect_uri: determineRedirectURI(req), // @todo fix,
      });

      // console.log('Auth code exchange result received:', results);

      // We have an access token and the user identity now.
      const accessToken = results.access_token;
      const suuntoAppUserName = results.user;

      // Create a Firebase account and get the Custom Auth Token.
      let firebaseToken;
      if (signInWithService) {
        firebaseToken = await createFirebaseAccount(suuntoAppUserName, accessToken);
      }
      return res.jsonp({
        firebaseAuthToken: firebaseToken,
        serviceAuthResponse: <ServiceTokenInterface>{
          accessToken: results.access_token,
          refreshToken: results.refresh_token,
          tokenType: results.token_type,
          expiresAt: currentDate.getTime() + (results.expires_in * 1000),
          scope: results.scope,
          userName: results.user,
          dateCreated: currentDate.getTime(),
          dateRefreshed: currentDate.getTime(),
        },
        serviceName: ServiceNames.SuuntoApp
      });
    } catch (error) {
      return res.jsonp({
        error: error.toString(),
      });
    }
  });
});

上面的代码没有找到名称为state的cookie

所以这里失败了

if (!req.cookies.state) {
        throw new Error('State cookie not set or expired. Maybe you took too long to authorize. Please try again.');
      } else if (req.cookies.state !== req.query.state) {
        throw new Error('State validation failed');
      }

在这里做了更多的搜索是一些更多的信息。

我基于https://github.com/firebase/functions-samples/tree/master/instagram-auth的例子

看起来其他用户也遇到了同样的问题https://github.com/firebase/functions-samples/issues/569

我也打开了这个问题https://github.com/firebase/firebase-functions/issues/544

【问题讨论】:

  • 我对“未禁用跨域 cookie”这句话特别感到困惑。 Cookie 本质上不能跨域共享。这个答案可能会有所帮助,尽管它可能只与 Firebase 函数有关:stackoverflow.com/a/44935288
  • @Grant Timmerman 我是说当用户启用跨域 cookie 时,一切正常。如果您看到我的函数在同一个域上,那么我不明白为什么将它们(保存 cookie 时)视为跨域。看一看。两个函数都在同一个域上。一个设置一个cookie,另一个读取它。但是当用户禁用了跨域 cookie 时,第二个函数无法读取任何内容。
  • @Grant Timmerman。抱歉,这是一个错字并已修复。
  • @JimmyKane,你能从 Inspector 选项卡中显示 cookie 吗?还要确保 cookie 是用 / 生成的,否则如果它是用路径 /authRedirect 或其他东西生成的,它将不起作用。我建议您更新问题中的Set-Cookie cookie 响应,同时确保也具有正确的域
  • @TarunLalwani 我按照你的要求添加了所有信息。感谢您的宝贵时间

标签: firebase oauth google-cloud-functions cross-domain


【解决方案1】:

您的响应显示 statesignInWithService cookie 的 Set-Cookie 标头,但没有 domain 属性:

set-cookie: state=1c8073866d1ffaacf2d4709090ad099872718afa; Max-Age=3600; Path=/; Expires=Sat, 03 Aug 2019 09:55:18 GMT; HttpOnly; Secure
set-cookie: signInWithService=false; Max-Age=3600; Path=/; Expires=Sat, 03 Aug 2019 09:55:18 GMT; HttpOnly; Secure

没有域的Set-Cookie 意味着cookie 在返回服务器的过程中发生的情况取决于浏览器。 “默认”,符合规范的行为:浏览器将获取服务 URL 的 FQDN 并将其与 cookie 相关联。 RFC6265:

除非 cookie 的属性另有说明,否则 cookie 是 仅返回到原始服务器(而不是,例如,任何 subdomains)...如果服务器省略 Domain 属性,则用户代理 只会将 cookie 返回到源服务器。

当浏览器决定是否接受来自 HTTP 服务的 cookie 时,其中一个决定标准是 cookie 是 第一方 还是 第三方 -派对

  • 第一方cookie:如果您请求的触发europe-west2-quantified-self-io.cloudfunctions.net/authRedirect调用的资源(网页)位于https://europe-west2-quantified-self-io.cloudfunctions.net/...
  • 第三方cookie:如果您请求的触发europe-west2-quantified-self-io.cloudfunctions.net/authRedirect调用的资源(网页)位于https://some.domain.app.com/...

在您的情况下,您的“父”应用/页面的 FQDN 可能与 europe-west2-quantified-self-io.cloudfunctions.net 不同,因此这些 cookie 被标记为第三方。如您所见,用户可以选择阻止第三方 cookie。自 2019 年 8 月起,Firefox 和 Safari 默认阻止第三方 cookie。大多数(如果不是全部)广告拦截器和类似的扩展程序也会阻止它们。这将导致浏览器简单地忽略来自europe-west2-quantified-self-io.cloudfunctions.net/authRedirect 的 HTTP 响应中的 Set-Cookie 标头。 cookie 不会被发送回europe-west2-quantified-self-io.cloudfunctions.net/authToken 的第二个 Firebase 函数,因为它在客户端上不存在。

您的选择:

  1. 在同一个域中托管您的应用和 Firebase 函数。
  2. 所有 HTTP 请求(应用程序和 Firebase 函数)都流经应用程序的架构;后者充当函数调用的各种代理。这是 one way 在 Firebase 中执行此操作。
  3. 假设您的应用和 Firebase 函数驻留在不同的域中。在 Javascript 中,您可以创建一小块调用 /authRedirect FB 函数的中间件,解析响应(包括通过 Set-Cookie 标头的 cookie),然后通过 @987654337 将响应(包括 cookie)写回浏览器@。在这种情况下,cookie 将是第一方的。
  4. 根本不使用cookies。您针对 cloudapi-oauth.suunto.com 执行的 oAuth 授权授予流程,因为授权服务器不需要 cookie。您遵循了推荐此流程的 instagram-auth 示例

单击“使用 Instagram 登录”按钮时,会显示一个弹出窗口,其中 将用户重定向到重定向函数 URL。

然后重定向功能将用户重定向到 Instagram OAuth 2.0 同意屏幕(仅第一次)用户必须授予批准。此外,state cookie 在客户端上设置为 state URL 查询参数的值,以便稍后检查。

当授权服务器不支持 PKCE 扩展(cloudapi-oauth.suunto.com 不支持)时,对 state 查询参数的检查基于 oAuth 客户端的 implementation best practice

客户端必须阻止 CSRF。一次性使用的 CSRF 代币携带在 “state”参数,安全绑定到用户代理,应该 用于该目的。如果客户端使用 PKCE [RFC7636] 并且 授权服务器支持 PKCE,客户端可以选择不使用 CSRF 保护的“状态”,因为这种保护由 PKCE 提供。 在这种情况下,“状态”可以再次用于其原来的目的, 即传输有关客户端应用程序状态的数据

关键短语安全地绑定到用户代理。对于 web 应用程序,cookie 是实现此绑定的一个不错的选择,但它不是唯一的选择。您可以将 state 的值粘贴到本地或会话存储中,单页应用程序在实践中正是这样做的。如果您想住在云端,您可以将 state 粘贴在 Cloud Storage 或同等设备中……但您必须制作一个唯一标识您的客户端此特定 HTTP 请求的密钥。并非不可能,但对于一个简单的场景来说可能有点矫枉过正。

【讨论】:

  • 只是为了确定。我在europe-west2-quantified-self-io.cloudfunctions.net 上设置了状态cookie,然后重定向到cloudapi-oauth.suunto.com,并以state 作为参数重定向回europe-west2-quantified-self-io.cloudfunctions.net,但在重定向回来时,我无法读取cookie 来验证参数国家的。例如,从 firebase 的这个 repo 中看到(他们做同样的事情)github.com/firebase/functions-samples/tree/master/…
  • 我正在尝试做的是针对服务对用户(已登录的 firebase 用户)进行身份验证,以便能够获取令牌,然后对该服务进行 API 调用。非常感谢您的解释,但我认为我对自己做错了什么仍然有些困惑。
  • @JimmyKane 您要调用的目标服务是什么?
  • 如您所见的 suunto。因此,用户从我的应用程序重定向到设置 cookie 和客户端机密并重定向到 suunto 服务的云功能。在那里,他进行身份验证并被重定向回一个云函数,该函数选择状态参数 + 代码来请求令牌。第二个函数(在服务身份验证之后)无法读取为同一域设置的第一个函数的 cookie(如果第 3 方 cookie 关闭)
  • 第二个函数(在服务验证之后)无法读取为同一域设置的第一个函数的 cookie(如果第 3 方 cookie 关闭) - 请更新您的问题有这个细节。显示如何调用第二个函数的代码和/或请求/响应标头
猜你喜欢
  • 2020-11-06
  • 2011-02-25
  • 2011-02-12
  • 2012-04-11
  • 1970-01-01
  • 2011-03-15
  • 2021-08-18
  • 2012-04-08
  • 2014-09-28
相关资源
最近更新 更多