I have successfully implemented authentication using gmail account in my app.
The problem is when the user signs-in again, the browser automatically picks the previous account which breaks the flow as I will explain below. p
Based on my research, adding prompt: "select_account"
here should have solved the issue. But, it had no effect.
router.get(
"/auth/google",
passport.authenticate("google", {
scope: [
"email",
"profile",
],
prompt: "select_account",
})
);
Here's how automatically picking the user account that was previously used to sign-in breaks the sign-in if the user tries to sign-in again.
This is how the sign-in works:
This endpoint is called from the frontend:
router.get(
"/auth/google",
passport.authenticate("google", {
scope: [
"email",
"profile",
],
prompt: "select_account",
})
);
After, the user picks an account, he is redirected to this callback endpoint:
router.get(
"/auth/google/callback",
passport.authenticate("google", {
failureRedirect: baseFrontendUrl,
session: false,
}),
function (req, res) {
User.findOne({ _id: req.user._id })
.then((user) => {
const payload = {
id: req.user._id,
};
console.log("🚀 ~ file: users.js:178 ~ .then ~ payload", payload);
jwt.sign(
payload,
keys.SecretKey,
{ expiresIn: 3600 * 24 * 356 },
(error, token) => {
if (error) {
res.status(400).json({
message: "Error logging in user.",
});
} else {
const redirect_url = `${baseFrontendUrl}/OAuthRedirecting?token=${token}`;
res.redirect(redirect_url);
}
}
);
})
.catch((error) => {
res.status(500).json({
message: "An error occured authenticating user using google.",
});
});
}
);
The problem is that if the user does not pick an account, he does not get redirected to that endpoint. So the second sign-in fails.
A solution to this could be to force the user to pick an account every time he signs-in but I couldn't find a way to do this.
This is how the google passport strategy is implemented:
passport.use(
new GoogleStrategy(googe_passport_config, function (
request,
accessToken,
refreshToken,
google_profile,
done
) {
let name = !!google_profile._json.given_name
? google_profile.given_name
: "Name";
let surname = !!google_profile._json.family_name
? google_profile.family_name
: "Surname";
let email = !!google_profile._json.email ? google_profile.email : "";
User.findOne({ email: google_profile._json.email })
.then((user) => {
if (!!user) {
return done(null, user);
} else {
userServices
.registerUserThroughGoogleAuth(
name,
surname,
email,
google_profile.id
)
.then((created_user) => {
if (!!created_user) {
return done(null, created_user);
}
})
.catch((error) => {
const error_to_be_returned = new Error("Error creating user");
return done(error_to_be_returned, null);
});
}
})
.catch((error) => {
const error_to_be_returned = new Error("Error finding user");
return done(error_to_be_returned, null);
});
})
);
I added some console logs there and nothing gets logged the second time the user tries to sign-in. So it's not even getting called.
You may try to add a random parameter to the authentication request URL to force user to select an account every time they sign in. As your prompt: "select_account"
is not working might caused by the browser cached the user's previous login credentials and automatically signs them in without showing the select account prompt.
router.get(
"/auth/google",
(req, res, next) => {
req.session.google_oauth2_state = Math.random().toString(36).substring(2);
next();
},
passport.authenticate("google", {
scope: ["email", "profile"],
prompt: "select_account",
state: true,
})
);
The random parameter will cause the browser to treat the request as a new URL, even if the user has previously signed in to your app with the same account. state: true
option is set to include the state parameter in the authentication request. This is required for Google's OAuth2 protocol to prevent CSRF attacks
Another workaround is to combine select_account
and consent
to force the account selection page to show. Your select_account
is not working could be that the browser may have cached the user's previous login credentials and automatically signs them in without showing the select account prompt. Adding the consent
would force the user to select their account every time they sign in and will also ask for the user's permission again.
router.get(
"/auth/google",
passport.authenticate("google", {
scope: [
"email",
"profile",
],
prompt: "select_account consent",
})
);