在同一域中使用 firebase 函数的 oAuth 的跨域状态 cookie 问题
Cross domain state cookie issue for oAuth using firebase functions while on the same domain
我正在为 firebase 平台的用户实施 oAuth 登录。
除非用户禁用跨域 cookie,否则一切正常。
这是我所做的。
- 从我的 domain/app 用户被重定向到云功能。
- could 函数设置
state
cookie 并将用户重定向到 oAuth 提供程序。
- 用户登录到 oAuth 提供程序并被重定向回另一个函数以获取代码等。这就是问题所在
在上面的第 3 步中,如果用户已从其浏览器中禁用跨域方 cookie,则该函数无法读取任何 cookie。
两个函数都在同一个域中,如下面的屏幕截图所示。
有什么办法可以解决这个问题吗?我的方法做错了吗?
我无法理解为什么这 2 个函数被视为跨域。
更新以包含更多信息
要求:
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
请求Headers
: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
回复Headers
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
在那之后,一旦用户 returns 来自服务,他根据解析 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
您的回复显示 Set-Cookie header state
和 signInWithService
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 在返回服务器的过程中发生的事情是 browser-dependent。 “默认”,spec-compliant 行为:浏览器将获取服务的 FQDN URL 并将其与 cookie 相关联。 RFC6265:
Unless the cookie's attributes indicate otherwise, the cookie is
returned only to the origin server (and not, for example, to any
subdomains)...If the server omits the Domain attribute, the user agent
will return the cookie only to the origin server.
当浏览器决定是否接受 来自 HTTP 服务的 cookie 时,判断标准之一是 cookie 是否 first-party 或 third-party:
- First-party cookie:如果您请求的触发对
europe-west2-quantified-self-io.cloudfunctions.net/authRedirect
调用的资源(网页)位于 https://europe-west2-quantified-self-io.cloudfunctions.net/...
- Third-party cookie:如果您请求的触发对
europe-west2-quantified-self-io.cloudfunctions.net/authRedirect
调用的资源(网页)位于 https://some.domain.app.com/...
在您的情况下,您的“parent”app/page 的 FQDN 可能与 europe-west2-quantified-self-io.cloudfunctions.net
不同,因此这些 cookie 被标记为 third-party。正如您所发现的,用户可以选择阻止 third-party cookie。自 2019 年 8 月起,Firefox 和 Safari 默认阻止第 3 方 cookie。大多数(如果不是全部)广告拦截器和类似的扩展程序也会阻止它们。这将导致浏览器简单地忽略来自 europe-west2-quantified-self-io.cloudfunctions.net/authRedirect
的 HTTP 响应中的 Set-Cookie header。该 cookie 不会发送回 europe-west2-quantified-self-io.cloudfunctions.net/authToken
处的第二个 Firebase 函数,因为它在客户端上不存在。
您的选择:
- 在同一域中托管您的应用和 Firebase 函数。
- 所有 HTTP 请求(应用程序和 Firebase 函数)都流经应用程序的架构;后者充当函数调用的各种代理。 one way 在 Firebase 中执行此操作。
- 假设您的应用程序和 Firebase 函数位于不同的域中。在 Javascript 中,您可以创建一小段调用
/authRedirect
FB 函数的中间件,解析响应(包括通过 Set-Cookie header 的 cookie),然后写入响应(包括 cookie)通过 document.cookie
返回浏览器。在这种情况下,cookie 将是 first-party.
- 根本不使用 cookie。作为授权服务器,您针对
cloudapi-oauth.suunto.com
执行的 oAuth 授权授予流程不需要 cookie。您遵循了推荐此流程的 instagram-auth 示例
When clicking the Sign in with Instagram button a popup is shown which
redirects users to the redirect Function URL.
The redirect Function then redirects the user to the Instagram OAuth
2.0 consent screen where (the first time only) the user will have to grant approval. Also the state
cookie is set on the client with the
value of the state
URL query parameter to check against later on.
当授权服务器不支持 PKCE 扩展(cloudapi-oauth.suunto.com
不支持)时,针对 state
查询参数的检查基于针对 oAuth 客户端的 implementation best practice:
Clients MUST prevent CSRF. One-time use CSRF tokens carried in the
"state" parameter, which are securely bound to the user agent, SHOULD
be used for that purpose. If PKCE [RFC7636] is used by the client and
the authorization server supports PKCE, clients MAY opt to not use
"state" for CSRF protection, as such protection is provided by PKCE.
In this case, "state" MAY be used again for its original purpose,
namely transporting data about the application state of the client
关键短语安全绑定到用户代理。对于网络应用程序,cookie 是实现此绑定的一个不错的选择,但它不是唯一的选择。您可以将 state
的值保存到本地或 session 存储中,single-page 应用程序在实践中正是这样做的。如果您想住在云端,您可以将 state
存储在云存储或同等设备中……但您必须制造一个唯一标识您的客户端的密钥 和 这个特定的 HTTP 请求。并非不可能,但对于一个简单的场景来说可能有点矫枉过正。
我正在为 firebase 平台的用户实施 oAuth 登录。
除非用户禁用跨域 cookie,否则一切正常。
这是我所做的。
- 从我的 domain/app 用户被重定向到云功能。
- could 函数设置
state
cookie 并将用户重定向到 oAuth 提供程序。 - 用户登录到 oAuth 提供程序并被重定向回另一个函数以获取代码等。这就是问题所在
在上面的第 3 步中,如果用户已从其浏览器中禁用跨域方 cookie,则该函数无法读取任何 cookie。 两个函数都在同一个域中,如下面的屏幕截图所示。
有什么办法可以解决这个问题吗?我的方法做错了吗?
我无法理解为什么这 2 个函数被视为跨域。
更新以包含更多信息
要求:
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
请求Headers
: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
回复Headers
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
在那之后,一旦用户 returns 来自服务,他根据解析 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
所以这里失败了
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
您的回复显示 Set-Cookie header state
和 signInWithService
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 在返回服务器的过程中发生的事情是 browser-dependent。 “默认”,spec-compliant 行为:浏览器将获取服务的 FQDN URL 并将其与 cookie 相关联。 RFC6265:
Unless the cookie's attributes indicate otherwise, the cookie is returned only to the origin server (and not, for example, to any subdomains)...If the server omits the Domain attribute, the user agent will return the cookie only to the origin server.
当浏览器决定是否接受 来自 HTTP 服务的 cookie 时,判断标准之一是 cookie 是否 first-party 或 third-party:
- First-party cookie:如果您请求的触发对
europe-west2-quantified-self-io.cloudfunctions.net/authRedirect
调用的资源(网页)位于https://europe-west2-quantified-self-io.cloudfunctions.net/...
- Third-party cookie:如果您请求的触发对
europe-west2-quantified-self-io.cloudfunctions.net/authRedirect
调用的资源(网页)位于https://some.domain.app.com/...
在您的情况下,您的“parent”app/page 的 FQDN 可能与 europe-west2-quantified-self-io.cloudfunctions.net
不同,因此这些 cookie 被标记为 third-party。正如您所发现的,用户可以选择阻止 third-party cookie。自 2019 年 8 月起,Firefox 和 Safari 默认阻止第 3 方 cookie。大多数(如果不是全部)广告拦截器和类似的扩展程序也会阻止它们。这将导致浏览器简单地忽略来自 europe-west2-quantified-self-io.cloudfunctions.net/authRedirect
的 HTTP 响应中的 Set-Cookie header。该 cookie 不会发送回 europe-west2-quantified-self-io.cloudfunctions.net/authToken
处的第二个 Firebase 函数,因为它在客户端上不存在。
您的选择:
- 在同一域中托管您的应用和 Firebase 函数。
- 所有 HTTP 请求(应用程序和 Firebase 函数)都流经应用程序的架构;后者充当函数调用的各种代理。 one way 在 Firebase 中执行此操作。
- 假设您的应用程序和 Firebase 函数位于不同的域中。在 Javascript 中,您可以创建一小段调用
/authRedirect
FB 函数的中间件,解析响应(包括通过 Set-Cookie header 的 cookie),然后写入响应(包括 cookie)通过document.cookie
返回浏览器。在这种情况下,cookie 将是 first-party. - 根本不使用 cookie。作为授权服务器,您针对
cloudapi-oauth.suunto.com
执行的 oAuth 授权授予流程不需要 cookie。您遵循了推荐此流程的 instagram-auth 示例
When clicking the Sign in with Instagram button a popup is shown which redirects users to the redirect Function URL.
The redirect Function then redirects the user to the Instagram OAuth 2.0 consent screen where (the first time only) the user will have to grant approval. Also the
state
cookie is set on the client with the value of thestate
URL query parameter to check against later on.
当授权服务器不支持 PKCE 扩展(cloudapi-oauth.suunto.com
不支持)时,针对 state
查询参数的检查基于针对 oAuth 客户端的 implementation best practice:
Clients MUST prevent CSRF. One-time use CSRF tokens carried in the "state" parameter, which are securely bound to the user agent, SHOULD be used for that purpose. If PKCE [RFC7636] is used by the client and the authorization server supports PKCE, clients MAY opt to not use "state" for CSRF protection, as such protection is provided by PKCE. In this case, "state" MAY be used again for its original purpose, namely transporting data about the application state of the client
关键短语安全绑定到用户代理。对于网络应用程序,cookie 是实现此绑定的一个不错的选择,但它不是唯一的选择。您可以将 state
的值保存到本地或 session 存储中,single-page 应用程序在实践中正是这样做的。如果您想住在云端,您可以将 state
存储在云存储或同等设备中……但您必须制造一个唯一标识您的客户端的密钥 和 这个特定的 HTTP 请求。并非不可能,但对于一个简单的场景来说可能有点矫枉过正。