I have an app where Google OAuth2 used to work fine some time ago, but now it seems that something has changed and it's not working anymore. When I try to login with Google, I get the following message in the response body:
The verifyIdToken method requires an ID Token
At first I thought that the problem could be with the implementation of google-auth-library
on the Node back-end, but, as far as I can tell, it seems that it didn't change, unless I'm missing something.
Here's what I have on the front-end:
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import AuthProvider from "./context/AuthContext";
import { GoogleOAuthProvider } from "@react-oauth/google";
ReactDOM.render(
<GoogleOAuthProvider clientId={process.env.REACT_APP_GOOGLE_CLIENT_ID}>
<AuthProvider>
<App />
</AuthProvider>
</GoogleOAuthProvider>,
document.getElementById("root")
);
Login.js
import { useNavigate, useLocation } from "react-router-dom";
import { AuthContext } from "../context/AuthContext";
import { GoogleLogin } from "@react-oauth/google";
import axios from "../config/axiosConfig";
const isProduction = process.env.NODE_ENV === "production";
const authContext = useContext(AuthContext);
const navigate = useNavigate();
const { state } = useLocation();
const [message, setMessage] = useState(null);
function Login() {
function handleSuccess(response) {
axios
.post("/api/auth/google", { token: response.tokenId })
.then((res) => {
const { user, isAuthenticated } = res.data;
authContext.setUser(user);
authContext.setIsAuthenticated(isAuthenticated);
navigate(state?.path || "/diary");
})
.catch((err) => {
console.log(err);
});
}
function handleFailure(response) {
setMessage("Authentication failed");
}
return (
<GoogleLogin
className="google-btn"
redirectUri={
isProduction
? process.env.REACT_APP_CLIENT_URL_PROD
: process.env.REACT_APP_CLIENT_URL_DEV
}
onSuccess={handleSuccess}
onError={handleFailure}
theme="filled_blue"
width="312px"
/>
)
}
export default Login;
And, on the server-side, I have this:
auth.controller.js
const { OAuth2Client } = require("google-auth-library");
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
const google = async (req, res) => {
try {
const { token } = req.body;
const ticket = await client.verifyIdToken({
idToken: token,
audience: process.env.GOOGLE_CLIENT_ID,
});
const { given_name, family_name, email, sub } = ticket.getPayload();
User.findOne({ email: email }, (err, user) => {
if (!user) {
const newUser = new User({
first_name: given_name,
last_name: family_name,
email: email,
googleId: sub,
});
newUser.save();
generateTokens(newUser, res);
res.status(201).json({
isAuthenticated: true,
user: given_name,
});
} else if (user && !user.googleId) {
user.googleId = sub;
user.save();
generateTokens(user, res);
res.status(200).json({
isAuthenticated: true,
user: user.first_name,
});
} else {
console.log(err);
}
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
What it needs is just a few adjustments to how data is passed from the new @react-oauth/google
component and how this is handled by the Node back-end.
First, ensure that your application is wrapped by GoogleOAuthProvider
, just like it was done before in index.js
. There's nothing new here:
import { GoogleOAuthProvider } from "@react-oauth/google";
ReactDOM.render(
<GoogleOAuthProvider clientId={process.env.REACT_APP_GOOGLE_CLIENT_ID}>
<App />
</GoogleOAuthProvider>,
document.getElementById("root")
);
Note that the clientId
is already being passed.
When you click the Google login button, if nothing goes wrong, it will pass the credentials response that it gets back from Google to handleSuccess
function:
function Login() {
// HANDLE GOOGLE AUTHENTICATION
function handleSuccess(credentials) {
axios
.post("/api/auth/google", credentials)
.then((res) => {
// do something...
})
.catch((err) => {
console.log("Authentication failed: " + err);
});
}
return (
<GoogleLogin
onSuccess={(credentialResponse) => {
handleSuccess(credentialResponse);
}}
onError={handleFailure}
theme="filled_blue"
width="312px"
/>
)
}
export default Login;
This is what it's actually being passed to the back-end in the POST
request:
credentials
{
"clientId": "<YOUR_CLIENT_ID>",
"credential": "<ID_TOKEN>",
"select_by": "btn"
}
Now, this is what you'll want to target on your back-end when handling the POST
request coming from the front-end. Once you're able to properly verify the credentials, you can decode it to get user info and manipulate data however you want:
const { OAuth2Client } = require("google-auth-library");
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
const google = async (req, res) => {
const { credential, clientId } = req.body;
try {
const ticket = await client.verifyIdToken({
idToken: credential,
audience: clientId,
});
const { given_name, family_name, email, sub } = ticket.getPayload();
// do something...
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
Here's the decoded credential (more info here):
{
"iss": "https://accounts.google.com", // The JWT's issuer
"nbf": 161803398874,
"aud": "314159265-pi.apps.googleusercontent.com", // Your server's client ID
"sub": "3141592653589793238", // The unique ID of the user's Google Account
"hd": "gmail.com", // If present, the host domain of the user's GSuite email address
"email": "[email protected]", // The user's email address
"email_verified": true, // true, if Google has verified the email address
"azp": "314159265-pi.apps.googleusercontent.com",
"name": "Elisa Beckett",
// If present, a URL to user's profile picture
"picture": "https://lh3.googleusercontent.com/a-/e2718281828459045235360uler",
"given_name": "Elisa",
"family_name": "Beckett",
"iat": 1596474000, // Unix timestamp of the assertion's creation time
"exp": 1596477600, // Unix timestamp of the assertion's expiration time
"jti": "abc161803398874def"
}