Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NodeJS + Google Login + Firebase Functions results in decoding firebase session cookie failed

I require a Google Sign-In for a Firebase application, to get this done I used multiple sources:

  • auth-sessions provides a good out of the box example with NodeJS + Firebase + Google Login.
  • manage firebase session provides a sample NodeJS functions (not entire working solution) to work with Firebase Functions.

The problem:

When attempting to sign in into a Google account on production, the following error occurs when executing the code authenticate function below:

admin
    .auth()
    .verifySessionCookie(sessionCookie, true /** checkRevoked */)
    .then((decodedClaims) => {
        log("Decode success");

        // inbetween checks

        log("Successfully decoded and authenticated");
        next();
    })
    .catch((error) => {
        log("Error authenticating");          < ---- THIS IS THE PROBLEM
    ...
    });

This error ONLY occurs when on production i.e. deployed to Firebase. When testing locally using firebase emulators emulating hosting and functions only (auth, firestore, database, etc are all production), login is successful. When deploying, the login fails with the error below.

Error:

Decoding Firebase session cookie failed. Make sure you passed the entire string JWT which represents a session cookie. See https://firebase.google.com/docs/auth/admin/manage-cookies for details on how to retrieve a session cookie.


More Details:

Below is a high-level overview of steps/actions performed

Steps overview of actions performed

1. Visit any page e.g. /login
2. Click sign in with Google, execute the popup provider (see [here][3])
2. 
    1. Sign in with Google account
    2. Send token to firebase functions for verification i.e. `POST /sessionLogin`
3. Receive response (assume 200 OK)
4. Redirect to authenticated URL

The error is on the last step i.e. 4

This error occurs after successfully creating a session using the sample /sessionLogin code found here on firebase's site:

const auth = admin.auth();
auth.verifyIdToken(idToken).then(value => {
    debug("Token verified")
    return auth.createSessionCookie(idToken, {expiresIn})
        .then((sessionCookie) => {
            // Set cookie policy for session cookie.
            const options = {maxAge: expiresIn, httpOnly: true, secure: true};
            res.cookie('session', sessionCookie, options);
            // res.json(JSON.stringify({status: 'success'}));
            res.status(200).send("OK");
        }).catch((error) => {
            debug(error);
            res.status(401).send('UNAUTHORIZED REQUEST!');
        });
}).catch(reason => {
    debug("Unable to verify token");
    debug(reason);
    res.status(401).send('INVALID TOKEN!');
});

The logs respond with a Token verified and a status 200 is sent to the client.

The client then redirects to an authenticated URL /user/dashboard which performs the authenticate check (see below) which fails and redirects back to /login:

const authenticate = (req, res, next) => {
    
    log("Authenticating");
    // source: https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookie_and_check_permissions
    const sessionCookie = req.cookies.session || '';
    // Verify the session cookie. In this case an additional check is added to detect
    // if the user's Firebase session was revoked, user deleted/disabled, etc.

    return admin
        .auth()
        .verifySessionCookie(sessionCookie, true /** checkRevoked */)
        .then((decodedClaims) => {
            log("Decode success");

            // inbetween checks

            log("Successfully decoded and authenticated");
            next();
        })
        .catch((error) => {
            log("Error authenticating");
            if(error.errorInfo && error.errorInfo.code && error.errorInfo.code === "auth/argument-error") {
                debug(error.errorInfo.message);
                res.redirect('/user/login');
                return;
            }
            debug(error);
            // Session cookie is unavailable or invalid. Force user to login.
            req.flash("message", [{
                status: false,
                message: "Invalid session, please login again!"
            }])
            res.redirect('/user/login');
        });
};

which is middleware for the express app:

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    databaseURL: "https://my-company-default-rtdb.firebaseio.com",
    storageBucket: "gs://my-company.appspot.com"
});

const app = express();
app.use(cors({origin: true}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(morgan('dev'));
app.use(cookieParser('0000-0000-0000-0000-0000'))
app.set('trust proxy', 1) // trust first proxy
// Attach CSRF token on each request.
app.use(attachCsrfToken('/', 'csrfToken', (Math.random()* 100000000000000000).toString()));
app.use(session({
    secret: '0000-0000-0000-0000-0000',
    resave: false,
    name: '__session',
    store: new FirebaseStore({
        database: admin.database()
    }),
}));
app.use(flash());
app.use(authenticate);

// routes

exports.app = functions.https.onRequest(app);

Execution Logs:

1:12:02.796 PM app Function execution started

1:12:02.910 PM app Authenticating

1:12:02.910 PM app Attempting to verify session cooking

1:12:02.910 PM app Cookies: {}

1:12:02.911 PM app Error authenticating

1:47:41.905 PM app auth/argument-error

1:12:02.911 PM [app] Decoding Firebase session cookie failed. Make sure you passed the entire string JWT which represents a session cookie. See https://firebase.google.com/docs/auth/admin/manage-cookies for details on how to retrieve a session cookie.

1:12:02.937 PM [app] Function execution took 141 ms, finished with status code: 302


Update

Call to backend for auth:

const postIdTokenToSessionLogin = (idToken, csrfToken) => {
    return axios({
        url: "/user/sessionLogin",
        method: "POST",
        data: {
            idToken: idToken,
            csrfToken: csrfToken,
        },
    }).then(value => {
        console.log(value);
        if(value.status === 200) {
            window.location.assign("/user/dashboard");
        }
    }).catch(reason => {
        console.error(reason);
        alert("Failed to login");
    });
}

Client side call:

var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth()
    .signInWithPopup(provider)
    .then(async value => {
        firebase.auth().currentUser.getIdToken().then(idToken => {
            // const idToken = value.credential.idToken;
            const csrfToken = getCookie('_csrf');
            return postIdTokenToSessionLogin(idToken, csrfToken);
        }).catch(reason => {
            console.error("Failed to get current user token");
            alert(reason.message);
        });
    })/*.then(value => {
    window.location.assign("/user/dashboard")
})*/.catch((error) => {
    console.error("Failed to sign in with Google");
    alert(error.message);
});

Update 2:

Updated client-side axios request with the following, also added extra req.cookies logging

return axios({
    url: "/user/sessionLogin",
    method: "POST",
    withCredentials: true,
    data: {
        idToken: idToken,
        csrfToken: csrfToken,
    },
})

Extra logging:

4:43:23.493 PM app Function execution started

4:43:23.501 PM app Authenticating

4:43:23.501 PM app Creating session

4:43:23.502 PM app /sessionLogin Cookies: {"csrfToken":"19888568527706150","session":"eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3Npb24uZmlyZWJ..."}

4:43:23.503 PM app Token verified

4:43:23.503 PM app {"name":redacted ,"picture":"","iss":"","aud":"",...}

4:43:23.503 PM app ==============

4:43:23.503 PM app /sessionLogin#verifyIdToken Cookies: {"csrfToken":"19888568527706150","session":"eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3Npb24uZmlyZWJ..."}

4:43:23.634 PM app /sessionLogin#createSessionCookie Cookies: {"csrfToken":"19888568527706150","session":"eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3N..."}

4:43:23.634 PM app Cookie:

4:43:23.634 PM app "eyJhbGciOiJSUzI1NiIsImtpZCI6InRCME0yQSJ9.eyJpc3MiOiJodHRwczovL3Nlc3Npb24uZmlyZWJhc2UuZ29vZ..."

4:43:23.634 PM app ==============

4:43:23.643 PM app [0mPOST /user/sessionLogin [32m200[0m 139.036 ms - 2[0m

4:43:23.643 PM app Function execution took 150 ms, finished with status code: 200

4:43:24.131 PM app Function execution started

4:43:24.153 PM app Authenticating

4:43:24.153 PM app Attempting to verify session cooking

4:43:24.153 PM app Cookies: {}


Update 3

Rewrites enabled API & NodeJS access exactly as shown in firebase.json:

{
  "database": {
    "rules": "database.rules.json"
  },
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "hosting": {
    "site": "my-company-admin-portal",
    "public": "public",
    "rewrites": [
      {
        "source": "/api/**",
        "function": "api"
      },
      {
        "source": "**",
        "function": "app"
      }
    ],
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  },
  "storage": {
    "rules": "storage.rules"
  },
  "emulators": {
    "auth": {
      "port": 9099
    },
    "functions": {
      "port": 5001
    },
    "database": {
      "port": 9000
    },
    "hosting": {
      "port": 5000
    },
    "storage": {
      "port": 9199
    },
    "ui": {
      "enabled": true
    }
  }
}
like image 275
CybeX Avatar asked Sep 02 '21 12:09

CybeX


1 Answers

sessionCookie is undefined as in the code provided in the question.

// Authenticate middleware right now
const authenticate = (req, res, next) => {

  log("Authenticating");
  // No sessionCookie declared
  return admin
        .auth()
        .verifySessionCookie(sessionCookie, true /** checkRevoked */)
  // undefined passed here   ^^^
}

You must pass the cookie that you set after using createSessionCookie in verifySessionCookie method as shown below:

// updated authenticate middleware
const authenticate = async (req, res, next) => {
  try {
    log("Authenticating");

    // Read the value of cookie here
    const sessionCookie = req.cookies.session

    // Return unauthorized error if cookie is absent
    if (!sessionCookie) return res.sendStatus(401)

    const decodedClaims = await admin.auth().verifySessionCookie(sessionCookie, true)

    // cookie verified, continue 
  } catch (e) {
    console.log(e)
    return res.send("An error occurred")
  }
}
like image 156
Dharmaraj Avatar answered Sep 28 '22 09:09

Dharmaraj