I am implementing a oAuth login for a user for the firebase platform.
All works fine except if the user has disabled cross domain cookies.
Here is what I did.
state
cookie and redirects the user to the oAuth provider. On step 3 above the function cannot read any cookie if the user has disabled the cross domain party cookies from his browser. Both functions are on the same domain as seen below in the screenshot.
Is there any way I can remedy this issue? Am I doing something wrong in my approach?
I cannot understand why the 2 functions are treated as crossdomain.
Update to include more info
Request:
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
Request 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
Response 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
After that and once the user returns from the service he authenticated against the code that parses the cookies (or the function that handles that) is:
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(),
});
}
});
});
The above code does not find a cookie with the name state
So it fails here
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');
}
Did a little more search here is some more info.
The example I based on https://github.com/firebase/functions-samples/tree/master/instagram-auth
Looks like other users suffer from the same issue https://github.com/firebase/functions-samples/issues/569
I opened also this issue https://github.com/firebase/firebase-functions/issues/544
Your Response shows a Set-Cookie header for state
and signInWithService
cookies without a domain
attribute:
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 without a domain means that what happens to the cookie on the way back to the server is browser-dependent. The "default", spec-compliant behavior: the browser will grab the FQDN of the service URL and associate it with the 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.
When the browser decides whether to accept the cookie from a HTTP service, one of the decision criteria is if the cookie is first-party or third-party:
europe-west2-quantified-self-io.cloudfunctions.net/authRedirect
resides at https://europe-west2-quantified-self-io.cloudfunctions.net/...
europe-west2-quantified-self-io.cloudfunctions.net/authRedirect
resides at https://some.domain.app.com/...
In your case the FQDN of your "parent" app/page is likely different from europe-west2-quantified-self-io.cloudfunctions.net
, thus these cookies are labeled as third-party. As you found out, a user can choose to block third-party cookies. As of August 2019, Firefox and Safari block 3rd-party cookies by default. Most (if not all) ad blockers and similar extensions also block them. This would lead the browser to simply ignore Set-Cookie header in the HTTP response from europe-west2-quantified-self-io.cloudfunctions.net/authRedirect
. The cookie would not be sent back to 2nd Firebase function at europe-west2-quantified-self-io.cloudfunctions.net/authToken
because it doesn't exist on the client.
Your options:
/authRedirect
FB function, parses the response (incl. the cookies via Set-Cookie header), then writes the response (incl. cookies) back to the browser via document.cookie
. The cookies in this case would be first-party.cloudapi-oauth.suunto.com
as the authorization server does not require cookies. You followed an instagram-auth example that recommends this flowWhen 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.
The check against state
query parameter is based on an implementation best practice for oAuth clients when authorization servers don't support the PKCE extension (cloudapi-oauth.suunto.com
doesn't support it):
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
The key phrase is securely bound to the user agent. For web apps, a cookie is a decent option of implementing this binding but it's not the only option. You can stick the value of state
into local or session storage, single-page apps do exactly that in practice. If you want to live in the cloud, you can stick state
in Cloud Storage or equivalent...but you'd have to manufacture a key that uniquely identifies your client and this particular HTTP request. Not impossible but perhaps overkill for a simple scenario.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With