Search code examples
node.jsgoogle-oauthpassport.jspassport-google-oauth

Cookies not being set when login using Google Login and passport


I am using nodejs and react on heroku and passport js. I have code that logs a user in using google; however, a cookie is not being set for that user. The first time the user tries to log in using google, he is redirected back to the server endpoint and he get's an error "Unable to verify authorization request state".

Here's my code

  passport.use(new GoogleStrategy({
            clientID: process.env.GOOGLE_OAUTH_CLIENT_ID_LOGIN,
            clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET_LOGIN,
            callbackURL: process.env.DOMAIN_NAME + "/user/google/callback"
        },
        function(issuer, profile, cb) {
            User.findOrCreateGoogleUser(profile.id, profile.emails[0].value).then(result => {
                return cb(null, result[0]);
            }).catch (err => {
                console.dir(err);
                return cb(err, null, {
                    message: err
                })
            });
        }
    ));

passport.serializeUser((user, cb) => {
    cb(null, user.id);
});

passport.deserializeUser((id, cb) => {
    User.fetchById(id).then(result => {
        cb(null, result[0]);
    }).catch(err => {
        cb(err, null);
    });
});

router.get("/google/callback", (req, res, next) => {
    passport.authenticate("google", {session: false}, (err, user, info) => {
        if (err || !user) {
            console.dir(err);
            var message =
                info && info.message
                    ? encodeURIComponent(info.message + " Please try again or try another login method.")
                    : encodeURIComponent(
                    "There was an error logging you in. Please try another login method."
                    );
            return res.redirect(
                process.env.BASE_CLIENT_URL + "/login?error=" + message
            );
        }
        req.login(user, {session: false}, (err) => {
            if (err) {
                console.dir(err);
                return res.redirect(
                    process.env.BASE_CLIENT_URL +
                    "/login?error=" +
                    encodeURIComponent(
                        "Invalid User. Please try another account or register a new account."
                    )
                );
            }
            const payload = {
                sub: user.id,
            };
            const token = jwt.sign(payload, process.env.JWT_SECRET, {
                expiresIn: process.env.JWT_EXPIRESIN,
            });
            res.clearCookie("auth");
            res.cookie("auth", token);
            res.redirect(process.env.BASE_CLIENT_URL + "/loginsuccess");
        });
    })(req, res, next);
});

router.get(
    "/google",
    passport.authenticate("google", {
        scope: ["email"],
    })
);


const session = require("express-session");
app.use (
    session ({
        secret: "FMfcgzGllVtHlrXDrwtpNdhLRXlNtVzl18088dda1",
        resave: false,
        saveUninitialized: true,
        proxy: true,
        rolling: true,
        cookie: {
            expires: 60 * 60 * 24,
            secure: (app.get('env') === 'production'),
            sameSite: 'lax'
        }
    })
);

What am I doing wrong?


Solution

  • Figured it out. Was running into the same issue. Pretty much, the reason setting cookies via the auth callback URL is because cookies only get set when the request is coming from the same domain as your API. And when that /google/callback URL is hit, the request is coming from accounts.google.com instead of your app. So your server is responding with a cookie, but its responding to Google's request, not yours. The cookie is not automatically passed.

    To fix it, you can specify the domain of your cookie to match your URLs. In my situation, my API lives at api.example.com and my app lives at app.example.com. In this example I'm using the cookie package to set the headers.

    So instead of res.cookie, do:

    res.setHeader(
     'Set-Cookie',
      cookie.serialize('XSRF-TOKEN', YOUR_OBJECT, { // XSRF-TOKEN is the name of your cookie
        sameSite: 'lax', // lax is important, don't use 'strict' or 'none'
        httpOnly: process.env.ENVIRONMENT !== 'development', // must be true in production
        path: '/',
        secure: process.env.ENVIRONMENT !== 'development', // must be true in production
        maxAge: 60 * 60 * 24 * 7 * 52, // 1 year
        domain: process.env.ENVIRONMENT === 'development' ? '' : `.example.com`, // the period before is important and intentional
      })
    )
    

    In this example, the cookie is named XSRF-TOKEN with a value of whatever YOUR_OBJECT is. The Path attribute sets the path for which the cookie should be sent, and the Domain attribute specifies that the cookie should be sent to all subdomains of example.com. The Secure attribute ensures that the cookie is only sent over HTTPS, and the HttpOnly attribute ensures that the cookie is only accessible via HTTP(S) requests, not via client-side JavaScript.

    Once the cookie is set, the browser will send it back to app.example.com with any subsequent requests from accounts.google.com, allowing the server to identify the user and maintain their session.

    Note that setting a cookie with a domain attribute of .example.com will make the cookie accessible to all subdomains of example.com, including app.example.com. If you only want the cookie to be accessible to app.example.com, you can set the Domain attribute to app.example.com instead.