CSRF error when using passport-google-one-tap strategy

Viewed 34

I am trying to implement Google One Tap sign-in and Sign in With Google in an Express.js app. I am already using Passport.js with the local strategy for email/password sign in, so I want to continue using Passport for the Google authentication as well, so that I can have access to req.user and handle sessions the same way in both cases.

I've implemented the passport-google-one-tap strategy, based on the examples in the docs and in this repo.

When I click the one-tap prompt in the browser, it looks like it's working; I get a blue checkmark that says "verified." A session gets created in my database. However, no user gets created, and the browser stays on the same page; it doesn't go to either the success redirect or the failure redirect that I specified in my router function. In the browser console, I get the error "POST https://play.google.com/log?format=json&hasfast=true&authuser=0 401 (anonymous) @ VM22315:1" and a bunch of issues relating to verifying a csrf token, under the heading of "Indicate whether to send a cookie in a cross-site request by specifying its SameSite attribute."

I can see that the Google documentation gives some guidance on verifying a csrf token server-side, but I really want to use passport rather than handling this verification myself, for uniformity. The passport-google-one-tap strategy's source code looks like it should be handling this verification, so I'm not sure how to fix this error.

Here's my repo: https://github.com/celiackelly/classroom-job-board/tree/google-one-tap

server.js file:

if (process.env.NODE_ENV !== 'production') {
  require('dotenv').config({path: './config/.env'})
}

const express = require('express')
const app = express()
const path = require('path')
const PORT = process.env.PORT
const cors = require('cors')
const mongoose = require('mongoose')
const passport = require('passport')
const session = require('express-session')
const MongoStore = require('connect-mongo')(session)
const expressLayouts = require('express-ejs-layouts')
const flash = require('express-flash')
const logger = require('morgan')
const connectDB = require("./config/database");

// Passport config
require('./config/passport')(passport)

//Connect To Database
connectDB();

const mainRouter = require('./routes/main')
const usersRouter = require('./routes/users')

app.set('view engine', 'ejs')
app.set('layout', './layouts/layout')

app.use(express.static(path.join(__dirname, 'public'))) //Set up public folder to serve CSS, JS, and image files
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.use(expressLayouts)
app.use(logger('dev'))
// Sessions
app.use(
    session({
      secret: process.env.SESSION_SECRET,
      resave: false,
      saveUninitialized: false,
      store: new MongoStore({ mongooseConnection: mongoose.connection }),
    })
  )
  
// Passport middleware
app.use(passport.initialize())
app.use(passport.session())

app.use(
  cors({
    origin: "http://localhost:7500", 
    methods: "GET,POST,PUT,DELETE", 
    credentials: true
  })
)
app.use(flash())

// Lets you access currentUser in ejs files (to change the navbar links based on whether a user is signed in)
app.use(function(req, res, next) {
  res.locals.currentUser = req.user;
  next();
});
  
app.use('/', mainRouter)
app.use('/users', usersRouter)
 
app.listen(process.env.PORT || PORT, ()=>{
  console.log(`Server running on port ${PORT}`)
})  

Passport.js config file:

const LocalStrategy = require('passport-local').Strategy
// const GoogleStrategy = require('passport-google-oauth20').Strategy
const GoogleOneTapStrategy = require('passport-google-one-tap').GoogleOneTapStrategy
const refresh = require('passport-oauth2-refresh')
const mongoose = require('mongoose')
const User = require('../models/User')
// const moment = require('moment');

module.exports = function (passport) {
  passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
    User.findOne({ email: email.toLowerCase() }, (err, user) => {
      if (err) { return done(err) }
      if (!user) {
        return done(null, false, { msg: `Email ${email} not found.` })
      }
      if (!user.password) {
        return done(null, false, { msg: 'Your account was registered using Google. To enable password login, sign in using Google, and then set a password under your user profile.' })
        //need to add this functionality in user profile
      }
      user.comparePassword(password, (err, isMatch) => {
        if (err) { return done(err) }
        if (isMatch) {
          return done(null, user)
        }
        return done(null, false, { msg: 'Invalid email or password.' })
      })
    })
  }))

   passport.use(
    new GoogleOneTapStrategy(
      {
        clientID: process.env.GOOGLE_CLIENT_ID, 
        clientSecret: process.env.GOOGLE_CLIENT_SECRET, 
        redirectUri: process.env.GOOGLE_AUTH_CALLBACK,
        verifyCsrfToken: true, 
      },
      function (profile, done) {
        console.log(profile)
        if (req.user) {
          User.findById(req.user.id, (err, user) => {
            if (err) { return done(err); }
            user.googleId = profile.id;
            user.firstName = profile.name.givenName;
            user.lastName = profile.name.familyName;
            user.save((err) => {
              req.flash('info', { msg: 'Google account has been linked.' });
              done(err, user);
            });
          });
        } else {
        User.findOne({ googleId: profile.id }, (err, existingUser) => {
          if (err) { return done(err); }
          if (existingUser) {
            return done(null, existingUser);
          }
          User.findOne({ email: profile.emails[0].value }, (err, existingEmailUser) => {
            if (err) { return done(err); }
            if (existingEmailUser) {
              req.flash('errors', { msg: 'There is already an account using this email address. Sign in to that account and link it with Google manually from your user profile.' });
              req.session.save(function(err) {   
                console.log('session saved');
                done(err);
              });
            } else {
              console.log(profile)
              const user = new User();
              user.email = profile.emails[0].value;
              user.googleId = profile.id;
              user.firstName = profile.name.givenName;
              user.lastName = profile.name.familyName;
              user.save((err) => {
                done(err, user);
              });
            }
          });
        });
      }
      }
    )
  );

POST login in main.js router

router.post(
  "/auth/google/callback",
  passport.authenticate(
    "google-one-tap",
    { failureRedirect: "/login" },
    (err, user) => {
      console.log(err, 'failure in /routes/main.js POST to /auth/google/callback')
    }
  ),
  function (req, res) {
    console.log('success in /routes/main.js POST to /auth/google/callback')
    // Successful authentication, redirect home.
    res.redirect(`/users/${req.user._id}/dashboard`)
  }
);

layout.ejs file has the GSI client library script loaded in the head:

<script src="https://accounts.google.com/gsi/client" async defer></script>

login.ejs view:

<form action="/login" method="POST">
    <input type="email" name="email" placeholder="Email">
    <input type="password" name="password" placeholder="Password">
    <input type="submit">
</form>

<h2>Or Sign In With Google:</h2>
<div id="btn-sign-in-with-Google"></div>

Client-side JS:

window.onload = function () {
  google.accounts.id.initialize({
    client_id: "350532626407-nsnjic5jllfm5utanr5hnmjuaise2s41.apps.googleusercontent.com",
    login_uri: "http://localhost:7500/auth/google/callback" // We choose to handle the callback in server side, so we include a reference to a endpoint that will handle the response
  });
  // You can skip the next instruction if you don't want to show the "Sign-in" button
  google.accounts.id.renderButton(
    document.getElementById("btn-sign-in-with-Google"), // Ensure the element exist and it is a div to display correctly
    { theme: "outline", size: "large" }  // Customization attributes
  );
  google.accounts.id.prompt(); // Display the One Tap dialog
}

Error messages in browser console:

VM22315:1          POST https://play.google.com/log?format=json&hasfast=true&authuser=0 401
(anonymous) @ VM22315:1
_.l.send @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:131
_.we @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:150
fg @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:226
t @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:279
Gh.flush @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:279
(anonymous) @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:274
Mb @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:77
_.l.dispatchEvent @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:75
_.og.s @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:232
setTimeout (async)
_.og.start @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:232
Gh.log @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:276
Vh.log @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:290
(anonymous) @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:345
Promise.then (async)
Vi.i @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:345
(anonymous) @ /_/gsi/_/js/k=gsi.gsi.en.q5IctuGN1uA.O/am=0g/d=1/rs=AF0KOtVTdQ9OD1LQW9XOtkHOps0aGWfhTw/m=credential_button_library:348
(anonymous) @ button?theme=outline&size=large&client_id=350532626407-nsnjic5jllfm5utanr5hnmjuaise2s41.apps.googleusercontent.com&iframe_id=gsi_800444_867772&as=0xJDIFy5wERkZJzMzCYlzQ:355

Browser console issue: Indicate whether to send a cookie in a cross-browser request by specifying its same-site attribute

0 Answers
Related