Google-api-nodejs-client: Invalid grant error

Created on 29 Jan 2020  路  4Comments  路  Source: googleapis/google-api-nodejs-client

I have used the following guides

https://developers.google.com/identity/sign-in/web/server-side-flow
https://developers.google.com/calendar/quickstart/nodejs

Environment details

  • OS: mac Catalina 10.15.2 (19C57)
  • Node.js version: v13.5.0
  • npm version: 6.13.4
  • googleapis version: 39.2.0

Steps to reproduce

1> I have a react frontend website that produces a google button using npm module gapi-script. I get the thought to the authorization and the client receives the code as per https://developers.google.com/identity/sign-in/web/server-side-flow.
2> I then send the access code to a graphql end point which then gets stored in a DB.
3> I then have a nodejs app that reads this value out and useses it. I have a modified version of https://developers.google.com/calendar/quickstart/nodejs This is shown below.

import fs from "fs";
import { google } from "googleapis";
import readline from "readline";

export function authorize(credentials: any, accessData: any, callback: any) {
  const {client_secret, client_id, redirect_uris} = credentials.web;
  const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
  // Check if we have previously stored a token.
  console.log("what is accessData at authorization stage - " + JSON.stringify(accessData));
  const token = accessData.Item.access_token;
  console.log("what is token at authorization stage - " + JSON.stringify(token));
  if (!token) {
    return getAccessToken(oAuth2Client, accessData, callback);
  }
  oAuth2Client.setCredentials(token);
  callback(oAuth2Client);
}

export function getAccessToken(oAuth2Client: any, accessData: any, callback: any) {
    console.log("what is token at authorization stage - " + JSON.stringify(accessData.Item.auth_token));
    const test = "XXXXXXXXXXXXXX";
    oAuth2Client.getToken(test, (err: any, token: any) => {
      if (err) { return console.error("Error retrieving access token", err); }
      console.log("credentials parsed back from Google - " + JSON.stringify(token));
      oAuth2Client.setCredentials(token);
      // Store the token to synamodb for later program executions
      callback(oAuth2Client);
    });
}

export function listEvents(auth: any) {
  const calendar = google.calendar({version: "v3", auth});
  calendar.events.list({
    calendarId: "primary",
    maxResults: 10,
    orderBy: "startTime",
    singleEvents: true,
    timeMin: (new Date()).toISOString(),
  }, (err: any, res: any) => {
    if (err) { return console.log("The API returned an error: " + err); }
    const events = res.data.items;
    if (events.length) {
      console.log("Upcoming 10 events:");
      events.map((event: any, i: any) => {
        const start = event.start.dateTime || event.start.date;
        console.log(`${start} - ${event.summary}`);
      });
    } else {
      console.log("No upcoming events found.");
    }
  });
}

5> I get the response. For obvious resons I blanked out the security information with "XXXXXXXXXX"

    config: {
      method: 'POST',
      url: 'https://oauth2.googleapis.com/token',
      data: 'code="XXXXXXX"client_id="XXXXXXX"&client_secret="XXXXXXX"&redirect_uri="XXXXXXX"&grant_type=authorization_code&code_verifier=',
      headers: [Object],
      params: [Object: null prototype] {},
      paramsSerializer: [Function: paramsSerializer],
      body: 'code="XXXXXXX"&client_id="XXXXXXX"&client_secret="XXXXXXX"&redirect_uri="XXXXXXX"&grant_type=authorization_code&code_verifier=',
      validateStatus: [Function: validateStatus],
      responseType: 'json'
    },
    data: { error: 'invalid_grant', error_description: 'Bad Request' },
    headers: {
      'alt-svc': 'quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000',
      'cache-control': 'private',
      connection: 'close',
      'content-encoding': 'gzip',
      'content-type': 'application/json; charset=utf-8',
      date: 'Wed, 29 Jan 2020 21:54:53 GMT',
      server: 'scaffolding on HTTPServer2',
      'transfer-encoding': 'chunked',
      vary: 'Origin, X-Origin, Referer',
      'x-content-type-options': 'nosniff',
      'x-frame-options': 'SAMEORIGIN',
      'x-xss-protection': '0'
    },
    status: 400,
    statusText: 'Bad Request'
  },
  config: {
    method: 'POST',
    url: 'https://oauth2.googleapis.com/token',
    data:'code="XXXXXXX"&client_id="XXXXXXX"&client_secret="XXXXXXX"&redirect_uri="XXXXXXX"&grant_type=authorization_code&code_verifier=',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'User-Agent': 'google-api-nodejs-client/3.1.2',
      Accept: 'application/json'
    },
    params: [Object: null prototype] {},
    paramsSerializer: [Function: paramsSerializer],
    body: 'code="XXXXXXX"&client_id="XXXXXXX"&client_secret="XXXXXXX"&redirect_uri="XXXXXXX"&grant_type=authorization_code&code_verifier=',
    validateStatus: [Function: validateStatus],
    responseType: 'json'
  },
  code: '400'
}

Notes

I notice the following that suggests something is wrong.

1> code_verifier= is blank at the end of the data field response that comes back. Either I am not setting something or I am using the wrong client type.
2> I am miss understanding how this library works is wrong. that actually I need to request the access/refresh token straight away in the client and send the refresh/access token to my backend for future use.

That I can't just store the access code in a database and call it later on to perform actions.

Thanks!

triage me web

All 4 comments

okay my understanding is that I need to use the same clientID for this flow, I have now changed my setup so that the clientid used in react-frontend is the same as the one in the backend.

I still get the error, however I am trying to find the equiviland code -

GoogleTokenResponse tokenResponse =
          new GoogleAuthorizationCodeTokenRequest(
              new NetHttpTransport(),
              JacksonFactory.getDefaultInstance(),
              "https://oauth2.googleapis.com/token",
              clientSecrets.getDetails().getClientId(),
              clientSecrets.getDetails().getClientSecret(),
              authCode,
              REDIRECT_URI)  // Specify the same redirect URI that you use with your web
                             // app. If you don't have a web version of your app, you can
                             // specify an empty string.
              .execute();

in the gapi library

the call oAuth2Client.getToken( does not seem to be eqivilant)

Also noticed this - https://stackoverflow.com/questions/57597327/exchanging-authorization-code-for-access-token-with-googleapis-package-in-node

so people seem to have experienced the same problem

For all thows that come after me - Resolution
Prerequisites
If you have the same setup as me where your performing the exchange for to token not in the frontend browser react code but in a back end setup. I did the following things which seems to resolve the issue.

1> Add the secondary hostname/port in the Google Cloud platform > APIs& Services > Credentials> (your oauth ClientID.) > Authorised JavaScript origins. with your web apps URI.

for example, if you have localhost:3000 serving your react code and localhost:3002. You need to ad both. This applies to your production setup as well.

2> The authorization code is one time use only. Once you use it, if your did not store the refresh token/access token. You need to ask the user to auth again. It gives the precise same message.

3> There seems to be a time out on how long you can hold the Authorization token. Google can answer this better than me, however I noticed I got the same error if I stored a working legitimate Authorization token for more than an hour and it gave me the same message as above.

My advise is send back better erroring than data: { error: 'invalid_grant', error_description: 'Bad Request' },

Since all the above scenarios seem to have caused the same error. It was hard for me to debug.

So me and a friend were facing exactly the same issue as everyone in this thread, and we were already going crazy after checking 10x every single param but still ending up with a invalid_grant error...

We were also using this library with callbacks, just like this example:

OAuthClient.getToken(code, (error, response) => {
  if (error) return console.log('error', error);

  console.log('response', response);
});

Turns out that, after almost giving up, we gave a shot trying using promises (using async/await) instead of callbacks...
And it actually worked! Yup, miraculously...
After seeing it working with promises, we moved back to callbacks, just to confirm, and BOOM! there was that despicable invalid_grant, again!
We permanently switched back to async/await and this is now solved for us :)

This might not work for everyone, but it worked for us... heres an example of the working code:

try {
  const response = await OAuthClient.getToken(code);
  console.log('response', response);
} catch (error) {
  console.log('error', error);
}

I'll update this reply with the current googleapis package version we're using as soon as possible.

@t5unamie would you have a sample of your solution? I have been struggling with this for a while now. without success. thanks a lot

Was this page helpful?
0 / 5 - 0 ratings

Related issues

hainguyents13 picture hainguyents13  路  3Comments

suparnavg picture suparnavg  路  3Comments

ovaris picture ovaris  路  3Comments

joseparoli picture joseparoli  路  3Comments

eduDorus picture eduDorus  路  3Comments