Skip to content

No __session cookie in prod and no res.locals auth data (local and prod) #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Geoffrey-Pliez opened this issue Dec 1, 2022 · 17 comments

Comments

@Geoffrey-Pliez
Copy link
Contributor

Hi,

I have a project running in express.js.
in local mode (emulator), it generates __session cookie.
On the other hand, in production mode, the cookie is not generated.

Moreover, whatever the mode, res.locals remains empty.

Can you help me ?

@aaronSig
Copy link

aaronSig commented Dec 7, 2022

Im seeing this too.

I believe it's because the __FIREBASE_DEFAULTS__ cookie isn't set. That cookie contains the _authTokenSyncURL which instructs firebase to collect a session cookie when the auth token changes.

The Firebase Defaults cookie quite likely isn't set because Cloud Functions clears out cookies that aren't named __session. That would also explain why the _session cookie is working locally and not in live.

I'm also seeing res.locals empty locally and in live. Reading through the code I'm almost sure it's because there's an undocumented dev mode which doesn't initiate the authenticated apps (here).

Perhaps setting the __FIREBASE_DEFAULTS__ cookie locally will get the session cookie. And res.locals may then work when deployed.

@aaronSig
Copy link

aaronSig commented Dec 7, 2022

Ignoring the __FIREBASE_DEFAULTS__, I can create a session cookie by calling the URL to mint the cookie directly with the code from here.

The cookie seems right and correct however the SSR function now logs an error:

The caller does not have permission; Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on how to use and troubleshoot this feature.

@aaronSig
Copy link

aaronSig commented Dec 7, 2022

Setting the roles:

  • Firebase Admin SDK Administrator Service Agent
  • Service Account Token Creator

On the Default compute service account resolves the errors.

The firebase app and the currentUser are now visible in res.locals serverside.

@Geoffrey-Pliez
Copy link
Contributor Author

@aaronSig, could you share some of your code to see how you did it ?

@aaronSig
Copy link

aaronSig commented Dec 8, 2022

Sure. It's a rough hack at the moment but I put this component in the tree to mint the session cookie when the ID token is updated.

import { getAuth, User } from "firebase/auth";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { getOrInitFirebaseApp } from "../initFirebase";

export default function AuthSync() {

  useEffect(() => {
    const mintCookie = mintCookieFactory("/__session");
    let auth = getAuth(firebaseApp);

    let unsub = auth.beforeAuthStateChanged(async (user) => {
      await mintCookie(user);
    });

    let unsubToken = auth.onIdTokenChanged((user) => {
      if(user) { // onIdTokenChanged does fire a null on page refresh. Calling mintCookie with null will delete the cookie
        mintCookie(user);
      }
    });

    return () => {
      unsub();
      unsubToken();
    };
  }, []);

  return null;
}

const DEFAULT_ID_TOKEN_MAX_AGE = 5 * 60;
const authIdTokenMaxAge = DEFAULT_ID_TOKEN_MAX_AGE;
let lastPostedIdToken: string | undefined | null = null;

const mintCookieFactory = (url: string) => async (user: User | null) => {
  const idTokenResult = user && (await user.getIdTokenResult());
  const idTokenAge =
    idTokenResult &&
    (new Date().getTime() - Date.parse(idTokenResult.issuedAtTime)) / 1_000;
  if (idTokenAge && idTokenAge > authIdTokenMaxAge) {
    return;
  }
  // Specifically trip null => undefined when logged out, to delete any existing cookie
  const idToken = idTokenResult?.token;
  if (lastPostedIdToken === idToken) {
    return;
  }
  lastPostedIdToken = idToken;
  await fetch(url, {
    method: idToken ? "POST" : "DELETE",
    headers: idToken
      ? {
          Authorization: `Bearer ${idToken}`,
        }
      : {},
  });
};

If everything is correct you should see the __session cookie in the devtools.

Note you may still need to add permissions above to get the res.locals objects to work serverside.

I've only been able to get this working when deployed to cloudfunctions, it doesn't work for me in the emulator.

@Geoffrey-Pliez
Copy link
Contributor Author

For the res.locals problem, I see in my case that the value of customToken is null in the following code here.

if I go further, I see that FirebaseTokenGenerator generates an IAMsigner with value serviceAccountId = null, so it tries to get this value by calling function getAccountId but this function returns the message

and when I set the serviceAccountId value, I get a customToken but signInWithCustomToken returns the same result as you

"The caller does not have permission; Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on how to use and troubleshoot this feature."

i hope it can help

@aaronSig
Copy link

aaronSig commented Dec 9, 2022

Did you try (in the IAM page in the GCP console) setting the roles :

  • Firebase Admin SDK Administrator Service Agent
  • Service Account Token Creator

On the service account that is running your cloud function?

@Geoffrey-Pliez
Copy link
Contributor Author

After many tries I solved my res.locals problem. Actually my code retrieved a bad Google credential in my local project. So, just to test, I provide the local credential in the .env file and the res.locals is no longer empty.

Regarding the __session cookie, I see that on the prod side, the request to the url '/__session' is never sent.

@aaronSig
Copy link

Regarding the __session cookie, I see that on the prod side, the request to the url '/__session' is never sent.

There's a FIREBASE_DEFAULTS cookie that contains a param (_authTokenSyncURL). That param instructs the firebase SDK to fetch the session cookie.

This cookie gets set in test but doesn't get set in production. As far as I can tell this is because Cloud Functions scrub all cookies apart from the __session cookie.

So, in production, you need to call /__session yourself. The code I added above does just that: #54 (comment)


@jamesdaniels should the FIREBASE_DEFAULTS cookie be set when deployed to Cloud Functions?

@Geoffrey-Pliez
Copy link
Contributor Author

I don't think the values are retrieved via the FIREBASE_DEFAULT cookie but via the .env file in the root of the function

@aaronSig
Copy link

It's in both but clientside the firebase-js-sdk uses that cookie.

Here's the code where that happens: https://github.com/firebase/firebase-js-sdk/blob/fdd4ab464b59a107bdcc195df3f01e32efd89ed4/packages/auth/src/platform_browser/index.ts#L89

@jamesdaniels
Copy link
Collaborator

@aaronSig the FIREBASE_DEFAULT cookie should come along with any **/*.js file when deploying w/the experiment (and inclusion of the JS SDK was detected). Cloud Functions uses the environment variable (.env) method. You can see the latter in your code-generated Cloud Function in .firebase/SITE_ID/functions/.env

@jamesdaniels
Copy link
Collaborator

the request to __session only happens in the beforeAuthStateChanged lifecycle event, so you will have to log out / back in, to trip it once you fix credential / service account scope issues.

@Geoffrey-Pliez
Copy link
Contributor Author

Isn't GOOGLE_APPLICATION_CREDENTIALS retrieved automatically from firebase hosting servers?

@jamesdaniels
Copy link
Collaborator

@Geoffrey-Pliez yes, the service account is built into the Cloud Functions environment—this allows the Admin SDK to initialize. FIREBASE_DEFAULTS is the equivalent that allows for the client-side SDK to be able to auto initialize in much the same manor—it is not built in at this point—so we inject it via Cookie (for CSR), environment variable (for SSG), and .env (for SSR) in Cloud Functions.

I have seen sporadic reports that the application default credentials don't have the right permission to mint the custom token out-of-box, more investigation is needed into why that scope isn't being included and if there's anything we can do to detect it before the error occurs.

@Geoffrey-Pliez
Copy link
Contributor Author

Geoffrey-Pliez commented Dec 16, 2022

Indeed, now I get the message on the server side :

#The caller does not have permission; Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on how to use and troubleshoot this feature.

or

Permission 'iam.serviceAccounts.signBlob' denied on resource (or it may not exist).

while {project-name}@appspot.gserviceaccount.com has service account token creator permission

If it can help anyone, here is my provisional code to force the creation of the __session cookie on the server :

const auth = await signInWithEmailAndPassword(getAuth(), this.login, this.password)
const idToken = await auth.user.getIdToken()
await fetch('/__session', {
        method: idToken ? "POST" : "DELETE",
        headers: idToken ? { Authorization: `Bearer ${idToken}` } : {}
      `});` 

to be adapted according to the method of connection

@Geoffrey-Pliez
Copy link
Contributor Author

a small step forward

I was actually giving permissions to the wrong account service. After a server-side debug, I see that the correct service was *****-compute@developer.gserviceaccount.com.
After giving it the "service account token creator" permission, res.locals are indeed created.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants