Search code examples
reactjsnode.jsoauth-2.0google-oauthtoken

Google OAuth2: 'The verifyIdToken method requires an ID Token' error


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 });
  }
};

Solution

  • 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"
    }