Louis Beaumont
Louis Beaumont

Reputation: 338

Querying Google Search Console on behalf of end-user: invalid_grant

I'm trying to query Google Search Console on behalf of my users, authenticated with Google, with the scope required for GSC.

I enabled Google Sign In, in Firebase Authentication, using the client id & secret in my script. Furthermore, I have set up my consent screen with a test email, the authorized domains, and the scopes "https://www.googleapis.com/auth/webmasters", "https://www.googleapis.com/auth/webmasters.readonly".

Here is my code

React + Firebase Client

Note that the user is already authenticated by email (we connect the email account with the Google account).

import {Grid, IconButton, List,
  ListItem, Tooltip, Typography} from "@mui/material";
import {GoogleAuthProvider, linkWithPopup,
  unlink} from "firebase/auth";
import React, {useState} from "react";
import {
  useUser,
} from "reactfire";
import {defaultErrorMessage} from "../../utils/constants";
import {log} from "../../utils/logs";
import {LoadingButton} from "@mui/lab";
import {Check, Clear} from "@mui/icons-material";


const GoogleSignIn = () => {
  const {data: user} = useUser();
  console.log("user", user);
  const onGoogleSearchConsole = async () => {
    const provider = new GoogleAuthProvider();
    provider.addScope("https://www.googleapis.com/auth/webmasters");
    provider.addScope("https://www.googleapis.com/auth/webmasters.readonly");
    provider.setCustomParameters({
      access_type: "offline",
    });

    linkWithPopup(user!, provider).then((result) => {
      console.log("onGoogleSearchConsole result", result);
    }).catch((error) => {
       console.log("onGoogleSearchConsole failed", error);
    });
  };
  const onDisconnectGoogleSearchConsole = () => {
    unlink(user!, "google.com").then(() => {
      // Auth provider unlinked from account
      console.log("onDisconnectGoogleSearchConsole");
    }).catch((error) => {
      console.log("onDisconnectGoogleSearchConsole failed", error);
    });
  };
  return (
    <React.Fragment>
      <Grid
        container
        direction="column"
      >
        <List>
          <ListItem
            secondaryAction={
              user &&
              user?.providerData.some((e) => e.providerId === "google.com") &&
              <Tooltip title="Unlink Google Search Console">
                <IconButton
                  onClick={onDisconnectGoogleSearchConsole}
                >
                  <Clear />
                </IconButton>
              </Tooltip>
            }
          >
            <Button
              color="primary"
              variant="contained"
              onClick={onGoogleSearchConsole}
              startIcon={
                user?.providerData.some((e) => e.providerId === "google.com") &&
                <Check/>
              }
              disabled={
                !user ||
                user?.providerData.some((e) => e.providerId === "google.com")
              }
            >
            Google Search Console
            </Button>
          </ListItem>
        </List>
      </Grid>
    </React.Fragment>
  );
};

export default GoogleSignIn;

Script to try to query GSC.

#!/usr/bin/python

from googleapiclient.discovery import build
import google.oauth2.credentials

CLIENT_ID = 'MY_CLIENT_ID'
CLIENT_SECRET = 'MY_CLIENT_SECRET'

# Here I currently copy paste my access token and refresh token from client
at = "MY_ACCESS_TOKEN_COPY_PASTED_FROM_CLIENT"
rt = "MY_REFRESH_TOKEN_COPY_PASTED_FROM_CLIENT"
tu = "https://oauth2.googleapis.com/token"
scopes = [
  "https://www.googleapis.com/auth/webmasters",
  "https://www.googleapis.com/auth/webmasters.readonly",
]
c = google.oauth2.credentials.Credentials(
    None, # seems that we can just use refresh token, otherwise still fail with "at"
    scopes=scopes,
    refresh_token=rt,
    token_uri=tu,
    client_id=CLIENT_ID,
    client_secret=CLIENT_SECRET,
)

# v3 or v1 both fail
# webmasters = build('webmasters', 'v3', credentials=c)

webmasters = build('searchconsole', 'v1', credentials=c)

# FAIL HERE
site_list = webmasters.sites().list().execute()
print("site_list", site_list)

The error is

google.auth.exceptions.RefreshError: ('invalid_grant: Bad Request', {'error': 'invalid_grant', 'error_description': 'Bad Request'})

Which is supposed to say an issue with the tokens? I tried all solutions from invalid_grant trying to get oAuth token from google without better results

I tried the sample here https://developers.google.com/webmaster-tools/v1/quickstart/quickstart-python (which seems to work only with a Desktop oauth client id), and it works, somehow, I tried to tweak their code, but it's kind of different scenario.

Did I do anything wrong? Thanks a lot :).

Upvotes: 0

Views: 91

Answers (1)

Louis Beaumont
Louis Beaumont

Reputation: 338

I solved the problem by using "gapi" (Google JS client) instead of Firebase. I used https://github.com/ph-fritsche/react-gapi for simplicity.

// component ...
  const scopes = [
    "https://www.googleapis.com/auth/webmasters",
    "https://www.googleapis.com/auth/webmasters.readonly",
  ];
  const gapi = useGoogleApi({
    scopes: scopes,
  });
  const gAuth = gapi?.auth2?.getAuthInstance();
  gAuth?.currentUser.listen((e) => console.log("user", e));

  const onGoogleSearchConsole = async () => {
    const refreshToken = await gAuth!.grantOfflineAccess();
    console.log("refresh token", refreshToken);
  };
  const onDisconnectGoogleSearchConsole = () => {
    gAuth!.currentUser.get().disconnect();
  };
// ...

Upvotes: 1

Related Questions