gogoogle-formsgoogle-forms-api

Google Forms API - How to retrieve a form


I'm trying to make a summary on responses for a Google Form. For that I use Google Forms API in Golang. I didn't find the Question text in the responses, just the QuestionId. So I decided to get the list of questions from the form itself. Surprisingly, I don't have issues with getting the responses, but I can't get the form.

I wrote the code based on Google Docs API Go Quickstart and modified it based on the Google Forms API doc.

Each time I run the program I get the Authentication error:

Unable to retrieve data from document: googleapi: Error 403: Request had insufficient authentication scopes.
Details:
[
  {
    "@type": "type.googleapis.com/google.rpc.ErrorInfo",
    "domain": "googleapis.com",
    "metadata": {
      "method": "google.apps.forms.v1.FormsService.GetForm",
      "service": "forms.googleapis.com"
    },
    "reason": "ACCESS_TOKEN_SCOPE_INSUFFICIENT"
  }
]

Code is below:

// Retrieves a token, saves the token, then returns the generated client.
func getClient(config *oauth2.Config) *http.Client {
    tokFile := "token.json"
    tok, err := tokenFromFile(tokFile)
    if err != nil {
        tok = getTokenFromWeb(config)
        saveToken(tokFile, tok)
    }
    return config.Client(context.Background(), tok)
}

// Requests a token from the web, then returns the retrieved token.
func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
    authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
    fmt.Printf("Go to the following link in your browser then type the "+
        "authorization code: \n%v\n", authURL)

    var authCode string
    if _, err := fmt.Scan(&authCode); err != nil {
        log.Fatalf("Unable to read authorization code: %v", err)
    }

    tok, err := config.Exchange(oauth2.NoContext, authCode)
    if err != nil {
        log.Fatalf("Unable to retrieve token from web: %v", err)
    }
    return tok
}

// Retrieves a token from a local file.
func tokenFromFile(file string) (*oauth2.Token, error) {
    f, err := os.Open(file)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    tok := &oauth2.Token{}
    err = json.NewDecoder(f).Decode(tok)
    return tok, err
}

// Saves a token to a file path.
func saveToken(path string, token *oauth2.Token) {
    fmt.Printf("Saving credential file to: %s\n", path)
    f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
    if err != nil {
        log.Fatalf("Unable to cache OAuth token: %v", err)
    }
    defer f.Close()
    json.NewEncoder(f).Encode(token)
}

var srv *forms.Service
var srvForm *forms.FormsService

func main() {
    ctx := context.Background()
    b, err := os.ReadFile("credentials.json")
    if err != nil {
        log.Fatalf("Unable to read client secret file: %v", err)
    }

    config, err := google.ConfigFromJSON(b, forms.FormsBodyReadonlyScope, forms.FormsResponsesReadonlyScope)
    if err != nil {
        log.Fatalf("Unable to parse client secret file to config: %v", err)
    }
    client := getClient(config)

    srv, err = forms.NewService(ctx, option.WithHTTPClient(client), option.WithScopes(forms.DriveScope, forms.FormsResponsesReadonlyScope))
    if err != nil {
        log.Fatalf("Unable to retrieve Docs client: %v", err)
    }

    srvForm = forms.NewFormsService(srv)

    if srvForm != nil {
        docId := "HERE GOES ID OF A FORM"
        doc, err := srvForm.Get(docId).Do()
        if err != nil {
            log.Fatalf("Unable to retrieve data from document: %v", err)
        }
        fmt.Printf("The title of the doc is: %s\n", doc.Items[0].Description)
        for _, v := range doc.Items {
            fmt.Printf("The description is: %s\n", v.Description)
            fmt.Printf("The title is: %s\n", v.Title)
            fmt.Printf("The title is: %s\n", v.QuestionItem.Question.QuestionId)
        }
    }
}

As shown in the examples here, it should be enough to provide "https://www.googleapis.com/auth/forms.body.readonly" as a Scope, but apparently it is not enough and even "https://www.googleapis.com/auth/drive" is not enough.

As you can see from the code, I've tried to provide the Authentication Scopes to google.ConfigFromJSON and to forms.NewService, but it doesn't help.


Solution

  • Apparently you can't just add the new scope to a program when token was created, it is necessary to delete the token and obtain new permission via the consent screen. After that code works fine.