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