Modify Google Sheets (API) Using PowerShell / Uploading CSV Files

PowerShell Generated Google Sheet
PowerShell Generated Google Sheet

I recently had a need to interact with a Google Sheets spreadsheet and it turned out to be pretty easy and worked very well! I wanted to write this post to share what I learned and hopefully it can help some others out there.

In this guide we’ll explore how to create a Google API key then and use PowerShell to create a new spreadsheets and upload a CSV file into Google Sheets.

Acquiring Google Sheets API

First head to the official Google Developers Console. If you have never used the Google API before go ahead and create a new project. I called mine Scripts-jamesachambers.

After creating a project it should take you to the main dashboard. From here we are going to click “ENABLE APIS AND SERVICES” at the top of the page. It looks like this:

Google Sheets Dashboard - Enable APIs and Services
Google Sheets Dashboard – Enable APIs and Services

This will bring up a page showing all the different Google APIs available. From here we are going to scroll down to the G-Suite section and click on “Google Sheets” and then choose the “Enable” button.

Google Developer Console - Add API Menu
Google Developer Console – Add API Menu
Google Sheets Enable Button
Google Sheets – API Enable Button

Enable Google Drive API

Now enable the Google Drive API using the exact same steps. This will be needed for some access/permission controls later.

To enable the Google Drive API follow the same steps we did in the “Acquiring Google Sheets API” but choose “Google Drive” from the G-Suite options instead.

Creating a Service Account

There is a lot of security built into Google’s APIs. Some types of them require user consent but can access private user data. The kind of account we want to create to work with Google Sheets using scripts is a service account.

This type of account uses a certificate to validate your identity instead of a username/password. It is able to create new spreadsheets and give permissions for others to view them without requiring user consent since the service account will own the spreadsheet sidestepping the private user data issue. It is also able to update these sheets right from PowerShell!

When you clicked the “Enable” button it should have taken you into the “Google Sheets API Console” but sometimes I’ve noticed the button just turns into “Manage”. Click Manage of you aren’t already in the Google Sheets developer console. You should be at a screen that looks like this:

Google Sheets API Credentials
Google Sheets API – Credentials Tab

Choose the “Credentials” tab on the left hand of the screen as shown above and click “Create Credentials” and choose the “Service Account” option.

Choose a name for your account. You can set an optional description here as well if you would like. Click the “Create” button and it will take you to the next screen.

Google Sheets API - Service Account Roles
Google Sheets API – Service Account Roles

Give your service account the “Project” – “Owner” role. This will give your service account full access to all resources in the project you created. You can create finer grained permissions if desired but the service account is only going to have access to the project you created so it should be safe unless you have other resources in the project.

Press the button again and leave the final screen on the defaults. Your service account is now created!

Create Service Account Access Key/Certificate

Now that we have created our service account we need to create an access key and download the certificate for it.

At the official Google Developers Console dashboard choose the “Service Accounts” tab from the left side of the screen and then click on the new service account you created:

Google Sheets - API Service Accounts Menu
Google API Service Accounts Menu

Make a note or copy of your service account’s email address listed here. This will be the $iss credential we’ll be using in PowerShell to authenticate with Google very shortly.

At the bottom of your “Service account details” page will be an option to add keys. Click the “Add Key” button and choose “Create new key” as shown here:

Google Sheets - Create API Key
Google API Service Account Details – Create New Key

Choose the “P12” format:

Google Service Account - Create P12 Key
Google Service Account – Create P12 Key

Press “Create”:

Google Sheets - Download API Key
Google Service Account – Private Key Download

A download dialog will pop up and the screen above will be displayed. This is your certificate file your scripts will use to access Google Sheets.

Warning: This is the only time Google will give you this key. If you lose it you cannot recover it or redownload it and have to create a new key and update all your scripts that use that key accordingly. Make sure you have this key backed up!

Once you have safely backed up this key we are all done with configuration and it’s time to head to PowerShell!

Install UMN-Google PowerShell Module

We are going to use the UMN-Google PowerShell module to be able to communicate with Google Sheet’s API. You will want to install this module on any machine you will be running scripts. Open PowerShell and type:

Install-Module UMN-Google

This PowerShell module is developed and maintained by the University of Minnesota. You can see more information at the PowerShell Gallery here: The UMN-Google PowerShellGallery Repository

Getting Authorization Token

This block will appear at the top of your scripts. It will use our service account certificate to get an authorization code from Google.

Create a new PowerShell .ps1 file with the editor of your choice. Copy and paste the code block below:

Import-Module UMN-Google

# Set security protocol to TLS 1.2 to avoid TLS errors
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# Google API Authozation
$scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
$certPath = "C:/Users/james/Desktop/PowerShell/jamesachambers-1551564735346.p12"
$iss = 'scripts@jamesachambers-1551564735346.iam.gserviceaccount.com'
$certPswd = 'notasecret'
try {
    $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss
} catch {
    $err = $_.Exception
    $err | Select-Object -Property *
    "Response: "
    $err.Response
}

$accessToken

Modify the values below to match your certificate file you downloaded in the previous step and your $iss (service account email address) information that we made a note of/copied earlier. The 2 lines you need to change to your own values are listed in bold.

I recommend fully qualifying the path to your certificate file to avoid working path problems (especially when doing background/automation scheduled tasks). Make sure you remember to copy the certificate from your downloads folder to the location where you created your PowerShell .ps1 file and that you change the path to yours.

After you have modified the values run the script to make sure you don’t receive any errors. If everything ran successfully you will see a valid Google API authorization token! It should look like this:

Testing Google Sheets
PowerShell – Google API Authorization Test

If your screen shows a long string of numbers/letters/symbols and does not give an error then congratulations, you’ve completed your API key setup successfully! If you received an error double check that you copied the certificate and gave the right path and run through the earlier steps quickly and verify everything looks correct.

Create New Google Sheet

Now that you have working authentication it’s time for the fun part! Now we get to start creating/modifying Google Sheet documents. Let’s create a new sheet and display the spreadsheet ID.

Add the following commands to the end of your .ps1 file with the title of your choice:

# Create new spreadsheet
$Title = 'Patching Spreadsheet'
$SpreadsheetID = (New-GSheetSpreadSheet -accessToken $accessToken -title $Title).spreadsheetId
$SpreadsheetID

# Create new sheet
$Sheet = 'Computers'
Add-GSheetSheet -accessToken $accessToken -sheetName $Sheet -spreadSheetID $SpreadsheetID

Now run your .ps1 file again:

Power Shell Google Sheets New Sheet
PowerShell – Google Sheets New Spreadsheet

The first line is your access token again, but the second line is the new spreadsheet ID! To access this spreadsheet you append the spreadsheet ID on the end of the following base URL:

https://docs.google.com/spreadsheets/d/

So for example, to access the spreadsheet I just created in this example go to https://docs.google.com/spreadsheets/d/1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM/

However, when you go to the URL you will see:

Google Sheets - You Need Access
Google Sheets – You Need Access

This is because by default these sheets are private. We need to set the permissions for other people to be able to access our new sheet!

Setting Google Sheet Permissions

Let’s start by adding individual users. Comment out the lines that create a new spreadsheet and sheet and add the following lines to the end of the file:

$SpreadsheetID = '1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM'
Set-GFilePermissions -accessToken $accessToken -fileID $SpreadsheetID -role writer -type user -emailAddress 'youremail@yourdomain.com'

Change the $SpreadsheetID to the ID you received in the last step and the email to match your own personal email address. This will add “writer” permissions to your account to the spreadsheet. The bolded lines are things you need to adjust. The final result should look like this:

Import-Module UMN-Google

# Set security protocol to TLS 1.2 to avoid TLS errors
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# Google API Authozation
$scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
$certPath = "C:/Users/james/Desktop/PowerShell/yourcertificate.p12"
$iss = 'scripts@jamesachambers-1551564735346.iam.gserviceaccount.com'
$certPswd = 'notasecret'
try {
    $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss
} catch {
    $err = $_.Exception
    $err | Select-Object -Property *
    "Response: "
    $err.Response
}

$SpreadsheetID = '1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM'

Set-GFilePermissions -accessToken $accessToken -fileID $SpreadsheetID -role writer -type user -emailAddress 'youremail@yourdomain.com'

Now run the script.

If you received the following error:

Invoke-RestMethod : The remote server returned an error: (403) Forbidden.

It means you very likely did not enable the “Google Drive” API. Go back to the “Enable Google Drive API” step from earlier and double check that the Google Drive API got enabled!

Now go back to your browser and refresh the “Access Denied” page. You should now see the blank spreadsheet!

Uploading a CSV File to Google Sheets

Now that you’ve set up your Google Sheets API keys and environment we are ready to modify new Google Sheets! If you already have a CSV file to upload you can use that, otherwise let’s create a quick testing CSV. Open Notepad or your favorite editor up to a new document and paste the following text:

Computer,"Operating System",RAM
"iMac 27""",OSX,"16 GB"
"HP ZBook 15""",Windows 10,"16 GB"

Now save the document as test.csv in the same directory you have our “Setting Google Sheets Permissions” PowerShell script from the last step. Remove or comment out the very last Set-GFilePermissions line of the script.

Google Sheets expects an ArrayList of values to upload to Google Sheets. We are going to load our test CSV and convert it to an ArrayList and submit the data to Google Sheets with the following code:

# Import CSV
$import = New-Object System.Collections.ArrayList($null)

# Build sheet header as it isn't included automatically
$import.Add( @("Computer", "Operating System", "RAM")) | Out-Null

# Build ArrayList
$inputCsv = Import-Csv "test.csv"
$inputCsv | ForEach-Object { 
    $import.Add( @($_.Computer, $_."Operating System", $_.RAM)) | Out-Null
}

# Upload CSV data to Google Sheets with Set-GSheetData
try {
    Set-GSheetData -accessToken $accessToken -rangeA1 "A1:C$($import.Count)" -sheetName "Computers" -spreadSheetID $SpreadsheetID -values $import -Debug -Verbose
} catch {
    $err = $_.Exception
    $err | Select-Object -Property *
    "Response: "
    $err.Response
}

This code loads the CSV into a variable called $inputCsv and then iterates through each object and adds it to the ArrayList we are going to send to Google Sheets. If you wanted to modify the data before sending it you absolutely can.

Once the CSV is imported you will have an object array and the names of the variables will be the name of the header of that column. You have to be careful if it has a quote in it. You can still access them by quoting the variable. For example:

$_."Operating System"

This will allow you to access the variables even if there are spaces in the column’s header. The actual first line of the CSV with the headers in it is consumed by PowerShell when you import it so we also create our own header line for our spreadsheet.

Your final code should look like this:

Import-Module UMN-Google

# Set security protocol to TLS 1.2 to avoid TLS errors
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

# Google API Authozation
$scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
$certPath = "C:/Users/james/Desktop/PowerShell/yourcertificate.p12"
$iss = 'scripts@jamesachambers-1551564735346.iam.gserviceaccount.com'
$certPswd = 'notasecret'
try {
    $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss
} catch {
    $err = $_.Exception
    $err | Select-Object -Property *
    "Response: "
    $err.Response
}

# Spreadsheet ID # of the Google Sheet
$SpreadsheetID = '1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM'

# Import CSV and build ArrayList
$import = New-Object System.Collections.ArrayList($null)
$inputCsv = Import-Csv "test.csv"
$inputCsv | ForEach-Object { 
    $import.Add( @($_.Computer, $_.'Operating System', $_.RAM)) | Out-Null
}

# Build sheet header as it isn't included automatically
$import.Add( @("Computer", "Operating System", "RAM")) | Out-Null

# Build ArrayList

# Upload CSV data to Google Sheets with Set-GSheetData
try {
    Set-GSheetData -accessToken $accessToken -rangeA1 "A1:C$($import.Count)" -sheetName "Computers" -spreadSheetID $SpreadsheetID -values $import -Debug -Verbose
} catch {
    $err = $_.Exception
    $err | Select-Object -Property *
    "Response: "
    $err.Response
}

Now we’re ready to run the code. Execute your script in PowerShell:

PowerShell - Test of uploading CSV to Google Sheets
PowerShell – Test of uploading CSV to Google Sheets

If everything was set up you should immediately see the changes in your browser window and it should look like this:

Patching Test Spreadsheet
Patching Test Spreadsheet

Wrapping Up

That’s it! You’ve now set up your Google API console access and can now modify Google Sheets through the power of PowerShell automation.

Make sure you check out the University of Minnesota’s GitHub repository to see any new changes or announcements related to the PowerShell extension. It is open source!

International Language Fix (Required for computers not set to “English” as the default language)

Once this post started gaining some traction I learned that international users (specifically ones that didn’t have “English” set as their default language) were having trouble with authentication. I suspected this was due to language differences because I’ve encountered it before on some of my other scripts (like my Minecraft Bedrock dedicated server setup script) that gained international traction as well and once we tested for it sure enough that was the issue.

Yoan graciously shared the solution in the comments:

Function Using-Culture (
[System.Globalization.CultureInfo]$culture = (throw "USAGE: Using-Culture -Culture culture -Script {scriptblock}"),
[ScriptBlock]$script= (throw "USAGE: Using-Culture -Culture culture -Script {scriptblock}"))
{
    $OldCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture
    trap 
    {
        [System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture
    }
    [System.Threading.Thread]::CurrentThread.CurrentCulture = $culture
    Invoke-Command $script
    [System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture
}

This was found at Microsoft Developer Blogs – Using Culture Culture ScriptBlock and the blog post contains a ton of information about how to use it and what it’s doing!

In a nutshell it lets you wrap your calls to UMN-Google (or your entire script if you wanted) like this:

Using-Culture en-US {$accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss}

This is a really easy way to wrap your functions if you are getting errors related to language differences / default headers / etc. and is definitely worth a try if your PC’s default language is not set to English!

90 thoughts on “Modify Google Sheets (API) Using PowerShell / Uploading CSV Files”

  1. Avatar for Mika

    Hi
    Why you can’t add more than 3 colums ?
    Try with :
    $import.Add( @("Computer", "Operating System", "RAM", "IP")) | Out-Null

    Error 400
    Ty

    1. Avatar for James A. Chambers

      Hey Mika,

      Great question! When you add more columns you need to adjust the line that posts the data to include more columns in the upload range:

      Set-GSheetData -accessToken $accessToken -rangeA1 "A1:C$($import.Count)" -sheetName "Computers" -spreadSheetID $SpreadsheetID -values $import -Debug -Verbose

      This code does the range of A1 to C(number of rows imported) which would only be 3 columns (A, B, C). To add an additional 4th column you would adjust the C to a D:

      Set-GSheetData -accessToken $accessToken -rangeA1 "A1:D$($import.Count)" -sheetName "Computers" -spreadSheetID $SpreadsheetID -values $import -Debug -Verbose

      Changing it to D will give you 4 columns and you would go along the alphabet further to add as many as you need. One thing to watch out for is that if you try to use columns that don’t exist yet in the sheet you may get an error (I believe a new sheet has columns A-Z by default so you’d have to be using more than 26 to run into this unless someone has deleted columns from the spreadsheet that has writer access). You can add more columns programmatically or just add writer permissions to your main Google account and you can go in there and add a bunch of new columns. I’ve used at least 20-30 columns before in some of my bigger intelligence gathering scripts (mostly for patching / updates) that had tons of scraped data etc.

      If the range we pass in -rangeA1 and the ArrayList’s size don’t match it will generate an error and if you log into the Google Sheets API developer console you’ll see errors logged in there from the 400 events (something like “invalid range” if I remember correctly) but they can be more useful than the extremely vague 400 code PowerShell returns for sure since it’s at least a hint (I think you can get more advanced error logging as well but I don’t think it was available in the “free” tier without some kind of upgrade/paid storage space/something like that). It does at least give you a very short general description of the error and how many of each there has been by default.

      Hopefully that helps!

  2. Avatar for Yoan

    Hello James,

    Thanks for your works and sorry for my english !

    I get the same error as some of the users,
    I read you answer to Elfs_rage and try to apply or your advidce :/

    I add the line -verbose so i saw that my certificate seems to be OK.
    I try without my proxy (domain free), from an other computer and i ‘ve got the same issue.
    I did not find any other tuto that explain how to connect to Google SpreadSheet Api trough Powershell so, if you have any other idea !

    PS C:\Temp\tosign> Import-Module UMN-Google

    add-type @"
    using System.Net;
    using System.Security.Cryptography.X509Certificates;
    public class TrustAllCertsPolicy : ICertificatePolicy {
    public bool CheckValidationResult(
    ServicePoint srvPoint, X509Certificate certificate,
    WebRequest request, int certificateProblem) {
    return true;
    }
    }
    "@
    [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

    # Set security protocol to TLS 1.2 to avoid TLS errors
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    # Google API Authozation
    $scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
    $certPath = "C:\Temp\tosign\*.p12"
    $iss = '*.gserviceaccount.com'
    $certPswd = 'notasecret'
    $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss -Verbose

    $accessToken

    Answer :

    COMMENTAIRES : Assembling RSA object based on given certificate file and password
    COMMENTAIRES : POST https://www.googleapis.com/oauth2/v4/token with -1-byte payload

    Invoke-RestMethod : Le serveur distant a retourné une erreur : (400) Demande incorrecte.
    Au caractère C:\Program Files\WindowsPowerShell\Modules\UMN-Google\1.2.12\UMN-Google.psm1:196 : 29
    + ... $response = Invoke-RestMethod -Uri "https://www.googleapis.com/oauth2 ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation : (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

    1. Avatar for James A. Chambers

      Hey Yoan,

      Thanks for the kind words! Everything definitely looks in order in your code. Just to test it I pulled up my own script and created test.csv from the example code and here was the output:

      PS Y:\James\PowerShell\GoogleSheets> y:\James\PowerShell\GoogleSheets\Test-Sheets.ps1
      VERBOSE: PUT https://sheets.googleapis.com/v4/spreadsheets/1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM/values/Computers!A1:C3?valueInputOption=RAW with -1-byte payload
      VERBOSE: received -1-byte response of content type application/json; charset=UTF-8

      spreadsheetId : 1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM
      updatedRange : Computers!A1:C3
      updatedRows : 3
      updatedColumns : 3
      updatedCells : 9

      I did this today with a brand new sheet via PowerShell and my service account at to rule out any cheating like reusing an existing sheet or anything like that and make sure it would work from scratch new.

      So the API / PowerShell extension is in working order (which I wanted to test since that was two people within a relatively short period with this issue). The question is what is going wrong for you guys that isn’t going wrong for me. I even tried it with backslashes instead of forward slashes like the path you gave and it worked in both cases.

      I have an idea. Let’s try adding some error handling like this (I added this in the guide as well if you want an easier place to copy/paste from that preserves the spacing):

      Import-Module UMN-Google

      # Set security protocol to TLS 1.2 to avoid TLS errors
      [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

      # Google API Authozation
      $scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
      $certPath = "Y:/James/PowerShellHome/Google/jamesachambers-1551564735346-46e9405204e7.p12"
      $iss = 'scripts@jamesachambers-1551564735346.iam.gserviceaccount.comd'
      $certPswd = 'notasecret'
      try {
      $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss
      } catch {
      $err = $_.Exception
      $err | Select-Object -Property *
      "Response: "
      $err.Response
      }

      I intentionally sabotaged my iss variable by adding a ‘d’ to the end of it like this for this test:

      $iss = 'scripts@jamesachambers-1551564735346.iam.gserviceaccount.comd'

      and this was the result:

      PS Y:\James\PowerShell\GoogleSheets> y:\James\PowerShell\GoogleSheets\Test-Sheets.ps1

      Status : ProtocolError
      Response : System.Net.HttpWebResponse
      Message : The remote server returned an error: (400) Bad Request.
      Data : {}
      InnerException :
      TargetSite : System.Net.WebResponse GetResponse(System.Net.WebRequest)
      StackTrace : at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest request)
      at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
      HelpLink :
      Source : Microsoft.PowerShell.Commands.Utility
      HResult : -2146233079

      Response:
      IsMutuallyAuthenticated : False
      Cookies : {}
      Headers : {Vary, X-XSS-Protection, X-Frame-Options, X-Content-Type-Options...}
      SupportsHeaders : True
      ContentLength : -1
      ContentEncoding :
      ContentType : application/json; charset=UTF-8
      CharacterSet : UTF-8
      Server : scaffolding on HTTPServer2
      LastModified : 7/18/2021 11:58:53 PM
      StatusCode : BadRequest
      StatusDescription : Bad Request
      ProtocolVersion : 1.1
      ResponseUri : https://www.googleapis.com/oauth2/v4/token
      Method : POST
      IsFromCache : False

      Now let’s compare that to if I have working authentication and sabotage the last line of my request to the server to give it invalid sheet data:

      PS Y:\James\PowerShell\GoogleSheets> y:\James\PowerShell\GoogleSheets\Test-Sheets.ps1
      VERBOSE: PUT https://sheets.googleapis.com/v4/spreadsheets/1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM/values/Computers!D1:C3?valueInputOption=RAW with -1-byte payload

      Status : ProtocolError
      Response : System.Net.HttpWebResponse
      Message : The remote server returned an error: (400) Bad Request.
      Data : {}
      InnerException :
      TargetSite : System.Net.WebResponse GetResponse(System.Net.WebRequest)
      StackTrace : at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest request)
      at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
      HelpLink :
      Source : Microsoft.PowerShell.Commands.Utility
      HResult : -2146233079

      Response:
      IsMutuallyAuthenticated : False
      Cookies : {}
      Headers : {Vary, X-XSS-Protection, X-Frame-Options, X-Content-Type-Options...}
      SupportsHeaders : True
      ContentLength : -1
      ContentEncoding :
      ContentType : application/json; charset=UTF-8
      CharacterSet : UTF-8
      Server : ESF
      LastModified : 7/19/2021 12:05:08 AM
      StatusCode : BadRequest
      StatusDescription : Bad Request
      ProtocolVersion : 1.1
      ResponseUri : https://sheets.googleapis.com/v4/spreadsheets/1anI7Bdr0ZjkSDwMbGpCb0KZc1SpSC-3JdQiycvoM5CM/values/Computers!D1:C3?valueInputOption=RAW
      Method : PUT
      IsFromCache : False

      Now these outputs look very similar (as I’ve said the development environment for getting errors is not the best) but this is definitely more information to go off than the guide previously had. There are definitely some differences in output that could prove useful. I’m wondering if it has to do with something like your default language and it needs a language header, I’m not totally sure yet. One difference I’m noticing is I’m hitting different servers for the different types of request (which makes sense, the “Server: scaffolding on HTTPServer2” seems to be the authentication server). For the request to put data I’m getting a server of “ESF”.

      I’m wondering what yours will be populated with (if at all). Just adding that rogue “d” character to the end of the ISS variable derailed the authentication enough for this and this is the best output I can think of to get from it. It should tell us ever so slightly more than we know now though! It sure seems like it’s credentials somehow though, but we can’t rule anything out like default languages / header settings etc. The API / code are fully functional, so that really only leaves the way the API key was set up / maybe the certificate has something unusual/wrong about it, I’m not sure yet.

      Are you positive you went in and did all the extra steps for enabling API access for Google Sheets (not just creating the key and cert) and all of that? It’s definitely multiple steps and I am wondering if people are creating the key and downloading the cert thinking they are done without setting all the rest of the permissions / additional steps like optional “Roles” step where we assign it owner permissions. You might have the right cert but not have actually enabled Google API sheets with the button / assigned the “Owner” role / something like that. Basically the place to fix something like this is in the Google API console though and not in code. It’s not authenticating basically even though code wise everything looks correct. Definitely quadruple check those keys / permissions / roles. Maybe even delete it and try setting up another one possibly. This *is* the setup basically as there’s pretty much nothing to the code other than the variable setup and one authentication line and it can’t be fixed there unless this does end up being something like a language header being required.

      You aren’t getting past the first authentication step from what I can see. Google isn’t accepting your certificate/username/etc. and this points to credentials / API key setup / permissions / etc. very strongly. There is a slight chance it could be default language / headers related though so I definitely want to see what your output looks like with the new error handling code.

      I do have one more idea for you. If you log into the Google Sheets API console at console.cloud.google.com. You should see a “Traffic by response code” chart that will have a blue 4xx errors link in the legend. Click on the 4xx and it will show you the errors you’re having.

      I’ll be honest, after checking it / validating the code / adding this error checking I’d be nearly willing to bet my life it’s permissions / credentials related except for the fact that your default language isn’t English and you’re the first person from what I can tell that has commented that is true for (at least that I can see from their output). I’m guessing others have figured this out after posting but they’re not going to come back and admit they screwed up the API key setup after calling out the code because it’s embarrassing (it shouldn’t be, I wrote the guide because it is tricky and I did it wrong many times before figuring it out, nothing to be embarrassed about here) but that is exactly the case most of the time.

      Can you try running it with the error handling and see what it returns?

      1. Avatar for Yoan

        Hey James,

        Thank you for your time ! I did exactly what you proposed and …. I get exactly the same error as yours (firs one).

        What is supprising is that i get a tokken ?

        >$accessToken

        ya29.a0ARrdaM8X9b2tgb9AlKvpJilRihOUnZAm-RENQIox84MknTuHUdO1c_LHBy2JPYEmRjylazXhLS89dBddqx2XgO4AbdndEqx5fL4zNd698gi6-fNqvfLnbf-dlc2bdGiZ3J6C0IWM2jm710e5suKw780Hrapm

        I asked some developers at my work place and they said that maybe we do not use the same library version as I install it yesterday, maybe yours is older ?

        I looked on google API dashboard and even if the url respond for this error is https://www.googleapis.com/oauth2/v4/token , I don’t have any error on my Google Developer dashboard.

        The “Data” field in the respond should contain the html body of the page right ?

        I though that the error was because of my environment.
        Indeed I’m beside a proxy and working on a domain (both Google workgroup and Active Directory). Of course I tried with my personal account and also from a computer without any proxy / firewall at home.

        Same result :/

        As a test, Do you think that you could create a “test account” so we can try with it ? This way, we will be sure that the error is not because of my service account ?
        Anyway I’m pretty sure i did exactly what you indicate :

        – create a project
        – add the APIs (sheet and drive)
        – add a service account
        – give him the owner permission
        – create a certificate than you have to download
        – the certPswd is always the same ‘notasecret’ (i created maybe 8 times an acount)
        – download this certificate in a accessible folder (i run my script as an admin) and try a get-content to see if i can oppen it

        here is my code and respond:

        Import-Module UMN-Google

        # Set security protocol to TLS 1.2 to avoid TLS errors
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

        add-type @"
        using System.Net;
        using System.Security.Cryptography.X509Certificates;
        public class TrustAllCertsPolicy : ICertificatePolicy {
        public bool CheckValidationResult(
        ServicePoint srvPoint, X509Certificate certificate,
        WebRequest request, int certificateProblem) {
        return true;
        }
        }
        "@

        [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

        # Google API Authozation
        $scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
        $certPath = "C:/Users/yoann.*Desktop/*.p12"
        $iss = '*.iam.gserviceaccount.com'
        $certPswd = 'notasecret'
        try {
        $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss
        } catch {
        $err = $_.Exception
        $err | Select-Object -Property *
        "Response: "
        $err.Response
        }

        $accessToken

        Status : ProtocolError
        Response : System.Net.HttpWebResponse
        Message : Le serveur distant a retourné une erreur : (400) Demande incorrecte.
        Data : {}
        InnerException :
        TargetSite : System.Net.WebResponse GetResponse(System.Net.WebRequest)
        StackTrace : à Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest request)
        à Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
        HelpLink :
        Source : Microsoft.PowerShell.Commands.Utility
        HResult : -2146233079

        Response:
        IsMutuallyAuthenticated : False
        Cookies : {}
        Headers : {Vary, X-XSS-Protection, X-Frame-Options, X-Content-Type-Options...}
        SupportsHeaders : True
        ContentLength : -1
        ContentEncoding :
        ContentType : application/json; charset=UTF-8
        CharacterSet : UTF-8
        Server : scaffolding on HTTPServer2
        LastModified : 20/07/2021 15:52:39
        StatusCode : BadRequest
        StatusDescription : Bad Request
        ProtocolVersion : 1.1
        ResponseUri : https://www.googleapis.com/oauth2/v4/token
        Method : POST
        IsFromCache : False

        ya29.a0ARrdaM8X9b2tgb9AlKvpJilRihOUnZAm-RENQIox84MknTuHUdO1c_LHBy2JPYEmRjylazXhLS89dBddqx2XgO4AbdndEqx5fL4zNd698gi6-fNqvfLnbf-dlc2bdGiZ3J6C0IWM2jm710e5suKw780Hrapm

        An other test if you want, i can send you by email the account certificate and email if you want to test.
        Can I ask you one more think ? In my previous comment I wrote the service account email, would you mind to delete/rename it in my comment, my CISO ask me to delete it.

        Thank you a lot for all your time, explanation and great work !

        New-Caledonian greeting,

        1. Avatar for James A. Chambers

          Hey Yoan,

          Welcome back and thanks for the additional information! I’ve scrubbed your previous post as requested (I totally understand) and if you would like to take a look and make sure I got everything they wanted removed (email and cert name, both identifiable but the email is the more serious one) it’s done. I wanted to leave the non-identifiable parts intact so that people can follow this conversation / learn from it later if that’s okay but if you would like me to remove anything else / delete the post still I will absolutely do so.

          Thanks for detailing the steps you took in the Google API console so well. It is strange you’re getting a token. Can you actually use this token and “ignore” the error? Like have you tried uploading a sheet yet by chance like the demo does using that token, or running New-GSheetSheet etc.? It’s even stranger you can’t see any errors in the console as that seems to point to it’s not getting to the right place for some reason, but then where could this token be coming from if it isn’t? Very curious. Maybe the authentication is successful (makes sense if you’re getting a token and there’s no errors in the console) but UMN-Google is choking on something else afterward (all theoretical / speculation but we’re going to start ruling things out here to narrow it down).

          For my version I’m just using the one that is installed from the Microsoft PowerShell Gallery when you do Install-Module UMN-Google. You can see the full package here. You can compare the version on the computer with this for a reference to see if we match. The current version has been out since July 2020 and has 10,000 downloads and every other version has like 10 downloads so it seems likely that almost everyone that has ever used this has used 1.2.12 and I suspect you will have the same version on your computer but this is worth checking.

          Is this a Enterprise account by chance? I’ve done this on both before but it might be worth trying on your personal account with the “free tier” personal Google API since companies/organizations can have additional permissions that could be impacting this. It would be a good test to rule out any Enterprise permissions that may be interfering at the very least. Have you also enabled the “Google Drive” API as well as Google Sheets? Some Enterprises also have forced 2FA enabled so the issue might actually be the account is getting 2FA’d but usually service accounts aren’t subject to this unless the organization is using some really heavy handed permissions / security configurations (not uncommon). Creating a personal one and trying from that can rule a lot of this stuff out.

          Do you have any computers available to you / the company / organization that have their default language set as “English”, or would it be possible to change one temporarily to English for a test? I’m not sure how internationally tested UMN-Google is as it was developed for the University of Minnesota and isn’t an “official” Google PowerShell extension or anything like that. There’s very little information out there on this subject (as you said before this is pretty much the only guide on the internet for it) so it’s possible that it hasn’t had a lot of international exposure and could have language related bugs like my scripts did before international users started trying to use them if that makes sense.

          I’m definitely very familiar with the issues caused by differences in languages / headers from my Minecraft Bedrock and Minecraft Java scripts though. To fix international language support on there I had to add language and identity headers to curl like this:

          curl -H "Accept-Encoding: identity" -H "Accept-Language: en"

          This was not necessary for people who had “en” as the default language and has no impact for them (and it didn’t originally have it at launch because mine didn’t need it to work, we had to learn this later). For international users who did not have ‘en’ as the default language though they would get a 403 Forbidden error. This is why I wanted to see if there was a computer with ‘en’ set available or if one could be set to it for a test here. It would really narrow things down for sure if we can rule in / rule out the default language and this is for sure a strong possibility.

          I can’t say for certain if UMN-Google has ever been tested in other languages yet (not that I’ve seen) so you may be the first one to validate whether a different language than ‘en’ is breaking this module or not. They may have not taken any of that stuff into account (which means the issue would actually be in the UMN-Google code itself). It may also have nothing to do with it which is why I’d like to start trying to rule some of this stuff out if we can.

          I’m definitely willing to try exchanging certs (especially if the ‘en’ language test doesn’t work / isn’t available) but it’s strange you aren’t getting any errors in the Google API console. I’m not sure how it could be giving you a token and not have any record of events in there. Would you be willing to try the ‘en’ language test?

          1. Avatar for Yoan

            Hey James Sorry for my late answer I already wrote it but i did not send the message.

            That worked ! You were right it was a language problem !!

            Here is a way to avoid it usinga function that change the curent powershell session.

            Function Using-Culture (
            [System.Globalization.CultureInfo]$culture,
            [ScriptBlock]$script)
            {
            $OldCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture
            trap
            {
            [System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture
            }
            [System.Threading.Thread]::CurrentThread.CurrentCulture = $culture
            $ExecutionContext.InvokeCommand.InvokeScript($script)
            [System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture
            }

            From the Microsoft Developer Blogs website

            I’m now trying to create spread sheet in a specific shared drive and no in the hidden drive own by the service account but it’s seems quite complicate. Have you ever do that ?

            Have a grat week,

            1. Avatar for James A. Chambers

              Hey Yoan,

              Wow, I’m so relieved you were able to get it working and grateful to you for posting this solution! That’s very interesting that PowerShell has a System.Globalization.CultureInfo class and that this is all serialized / built-in. I’ve never encountered it before (I believe it would be fair to say I’m “spoiled” having the OS software written in my native language) but it would be extremely useful in other situations where similar language-related issues may occur. Very neat trick!

              There are definitely a bunch of functions to help you out with this included in the UMN-Google PowerShell module. The way I usually find them (there’s hardly any documentation anywhere) is go here.

              Now use your browser’s search (Ctrl+F) and search for “Move”. You should see a Move-GFile in there with the full documentation on the parameters as well:

              function Move-GFile
              {
              .Synopsis
              Change parent folder metadata

              .DESCRIPTION
              A function to change parent folder metadata of a file.

              .PARAMETER accessToken
              OAuth Access Token for authorization.

              .PARAMETER fileID
              The fileID to move.

              .PARAMETER folderID
              The fileID of the new parent folder.

              .PARAMETER parentFolderID
              The fileID of the parentFolder. Optional parameter. root (My Drive) is assumed if not specified.

              .EXAMPLE
              MoveGFile -fileID 'String of File ID' -folderID 'String of folder's File ID'
              #>

              You’ll find many other useful functions in here that may be of assistance as well. Remember that our $SpreadsheetID variable is actually just a file ID within Google Drive. This is how we can use Set-GFilePermissions like I do in the guide on the $SpreadsheetID since in reality it is just a file.

              That means the only parameters we would still need to find is the name of the folder you want to move it to and the “parent ID”. If you Ctrl+F for “folder” it looks like a folder is just a type of file so you should be able to use Get-GFileID (search the link for usage) theoretically. You can also just go to the folder in your web browser:

              https://drive.google.com/drive/folders/1z0fEooPN3KBNNxziAmvmng14jWEE6d74

              The bolded part is the folder ID here. This stuff is usually easier to find in the browser since a Google Drive URL usually contains the same “parameters” that the functions UMN-Google wants to use in PowerShell require to get to the “same place”.

              I believe the “parentFolderID” parameter will also be necessary if you want to get this file out of the hidden service account’s drive (your original question). If you set a different parent folder ID this should replace the service account’s hidden default folder (which would be root (My Drive) of the service account if another one isn’t specified). You can find this ID the same way in the browser by just going “up” one folder to the root of the drive and examining the URL in your browser which will contain the information you need to find these files/folders/etc.

              The code should generally be used for as little as possible unless you really, really want to make it painful when Google already gives you all the frontend stuff with the API (and they want you to use it, I would theorize that the vague error codes and things like that are meant to strongly encourage/require this). But if you have a good use case, you can absolutely do this via code! You will also need to add the service account to both the “new” folder as well as the “parentID” folder’s permissions before you attempt any of this or you’ll definitely get the 400 errors etc.

              Thanks again for sharing this fix, I’m sure it will help a lot of others out and it will definitely let me help anyone else who encounters this much more quickly now that I have a confirmed fix working. Best of luck and let me know if you run into any more questions / need more help as I’m happy to look at any of it. Take care!

  3. Avatar for Elfs_rage

    not working
    error 400
    C:\Program Files\WindowsPowerShell\Modules\UMN-Google\1.2.12\UMN-Google.psm1:196 : 29
    + ... $response = Invoke-RestMethod -Uri "https://www.googleapis.com/oauth2 ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation : (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebEx
    ception
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

    After 2 tests…

    1. Avatar for James A. Chambers

      Hey Elfs_rage,

      That is just a generic REST error unfortunately. It could be wrong username / password, the wrong certificate, you didn’t put the fully qualified path to the certificate and it’s not looking where you think it is, etc. and just tells us that the server didn’t accept your request and returned an error code.

      Basically, unfortunately this tells us nothing more than the server didn’t accept your command! Sometimes it can be more unusual things such as using the wrong TLS version (there’s some PowerShell fixes for this if it happens) but it’s really difficult to help further with this. It could be anything!

      I would start with the basics. If it’s not getting this far it’s most likely something like not fully qualifying the path to your certificate or the API key not set up completely etc. We’re not doing anything too crazy in the example code so at this stage it’s something like wrong credentials / cert / API key / connection issues / etc.

      It’s really hard to say but I would say follow the guide precisely with no exceptions (including the fully qualified paths) for the best chance of success! Don’t make any assumptions, it seems relatively fragile and expects everything to be exactly how it wants it and gives nearly useless error messages. If you are building a spreadsheet and do something wrong it will give you the same REST error and if you look at the older comments you will see other people went through this too. Almost all of them ended up being successful but they too got generic difficult to troubleshoot REST errors and eventually narrowed down what they passed to the commands that it didn’t like.

      That’s one of the downsides of using this approach. For me once you set it up it just always works and works great but I had finicky problems getting it to work initially as well (like the path issue I mentioned earlier, it should be able to find “cert” in the working path/directory but it doesn’t, I had to fully qualify it for seemingly no reason, maybe it’s a security feature to prevent applications from picking up the wrong/malicious certs).

      If you need rock solid enterprise deployment reliability or a world class development environment then this may not be the way to go for sure! If it’s a super complicated sheet that is built programmatically rather than just dumping a CSV into Google Sheets it doesn’t provide a very good “development” environment with useful error messages if it makes sense. If you did everything 100% right it works, otherwise it will fail with a generic error and you have to figure out why yourself (difficult for sure).

      Basically the trick for this is if you can follow the guide exactly and get just the most basic upload to work (meaning you have successful authentication and can exchange data) that gives you your “baseline” for whatever application you are making. When you have that you can carefully add things and when you start getting generic REST errors you can fall back to a working baseline and narrow down from there exactly where things went off the rails. Hopefully that helps and let us know what you find!

  4. Avatar for Dallin

    Hi James, this is awesome! I was able to get it working. I am having issues when I want to add multiple CSV file’s data to the same spreadsheet via PowerShell. The first line of sheet keeps getting overwritten. Do you know the best way to upload CSV data so that it goes to a new line within the spreadsheet? I’m guessing it has something to do with the defined range during the Set-GSheetData command. Let me know your thoughts.

    Thank you!

    1. Avatar for James A. Chambers

      Hey Dallin,

      That’s a fantastic question! I actually have ran into the issue before with the first line of the sheet. I’m sure there’s a better way to do it but I always worked around it by throwing out the first line of the file and inserting my own headers like this:

      $inputCsv = Import-Csv "C:/Users/jchambers/Desktop/PowerShell/test.csv"
      $export = New-Object System.Collections.ArrayList($null)
      $inputCsv | ForEach-Object {
      $export.Add( @($_.ComputerName, $_.Model, $_.Users, $_.Keys)) | Out-Null
      }
      $export[0] = $("Computer Name", "Model", "Users", "Keys")

      Set-GSheetData -accessToken $accessToken -rangeA1 "A1:D$($import.Count)" -sheetName "BitlockerKeys" -spreadSheetID $SpreadsheetID -values $import -Debug -Verbose

      Basically in the code when I build the ArrayList the way that Google wants it to be uploaded I import all the lines and just throw out the first line and set export[0] (which represents the first line in the ArrayList, or index 0) to the values I want it to be for the first line.

      This isn’t a good solution if you actually have data in the first line but most of the time with CSVs the reason the first line is weird is that is considered the “header” line in a CSV. It’s a part of the CSV file format that doesn’t really jive well with Google Sheets (and is different across different platforms, CSV isn’t just one “standard” is part of the problem). From the wikipedia CSV page:

      The name “CSV” indicates the use of the comma to separate data fields. Nevertheless, the term “CSV” is widely used to refer to a large family of formats that differ in many ways. Some implementations allow or require single or double quotation marks around some or all fields; and some reserve the first record as a header containing a list of field names

      So basically since there isn’t a really solid set of standards on how the first line is handled (or if it even is considered a header) you end up with a lot of weirdness like this. Hopefully that explains at least where it comes from! Basically by using the first line as a header and building it yourself like I did in the export[0] line you can avoid any weirdness between applications and how they implement CSV as long as they don’t put actual data in that first line.

      If you absolutely had to have changing data in the first line I’m sure there’s a way to clean those up and handle it via code. I just wanted to let you know what I did when I ran into this to see if it would help!

  5. Avatar for Rich Hopkins

    This is great. I’m trying to come up with a way to make this more dynamic for any CSV file, but Set-GSheetData keeps blowing up on me and throwing loads of errors. At first I thought it was because you were specifying “A1:C” that I needed to first get the number of columns converted to change out C, so I came up with this so far, but Set-GSheetData still blows up. Any thoughts?

    #Upload CSV data to Google Sheets with Set-GSheetData
    #start by converting the number of columns into an excel Alphabet column
    #https://justaprogrammer.net/2012/01/09/using-powershell-to-represent-base-26-as-the-uppercase-english-alphabet/
    Function Convert-ToLetters {
    [OutputType([string])]
    Param
    (
    [Parameter(Mandatory = $true,
    Position = 0)]
    [ValidateNotNullOrEmpty()]
    [int]$value
    )

    $currVal = $value
    $returnVal = ''
    While ($currVal -ge 26) {
    $returnVal = [char](($currVal) % 26 + 65) + $returnVal
    $currVal = [int][math]::Floor($currVal / 26)
    }
    $returnVal = [char](($currVal) + 64) + $returnVal

    Return $returnVal
    }
    $columnCount = Import-Csv $inputFile | ForEach-Object {
    @($_.PSObject.Properties | Where-Object {
    $_.Value -ne $Null
    }).Count
    }
    $columnLetter = Convert-ToLetters -value $columnCount[0]
    Set-GSheetData -accessToken $accessToken -rangeA1 "A1:$($columnLetter)$($arrList.Count)" -sheetName $Sheet -spreadSheetID $SpreadsheetID -values $arrList -Debug -Verbose

    1. Avatar for James A. Chambers

      Hey Rich,

      It looks like you’re on the right track. One thing to watch out for from things I have seen in the past is to make sure that the columns actually exist on your spreadsheet. If you are trying to import a sheet that has more than 26 columns (or whatever the default amount of columns is on a new default spreadsheet) and they aren’t there I have seen it throw an error before.

      The number of fields you are passing in your ArrayList also needs to match the number of columns you are passing in that statement as the column letter. Unfortunately the error is very generic and unhelpful, but those are two things I have seen cause this before.

      I see that you are checking for null fields here but is it possible that could be causing a mismatch in how many fields are in the ArrayList and the letter it is using?

      1. Avatar for Richard Hopkins
        Richard Hopkins

        The problem was indeed with how I was building the ArrayList. I tried to shortcut importing the CSV file and ended up with the data not being setup quite right. Here is where I’m at now.

        Still working on a way to convert any CSV file a properly formatted ArrayList without knowing the header info first which is turning out to be trickier than I thought. In the mean time I can manually setup the headers and then feed the file in the way you are showing, then things work. EIther way, I hope you find the helper fuctions useful, thanks again for laying the groundwork.

        1. Avatar for James A. Chambers

          Hey Richard,

          Glad to hear it! I just wanted to throw out a few things to look out for. It’s really too bad that the error messages aren’t more descript but it seems like once you get a stable function working it works well enough.

          Thanks for posting your solution and such detailed instructions as well as the GitHub link!

          1. Avatar for Richard Hopkins
            Richard Hopkins

            FYI, check the link again, I updated Publish-GoogleSheets to take either a title or an ID, that way you can have it create a new sheet or wipe an existing one and replace it. I’ll be writing up my own blog on these eventually, but three kids and job keep me busy enough so it may be a while 🙂

  6. Avatar for Andrew

    Hi James,

    Thanks for the feedback.

    Unfortunately it didn’t work. I followed your tutorial step by step. I tried to force it to trust the cert but it didn’t work too.

    My server is MS2019, com powershell 5.1

    My script:

    Import-Module UMN-Google

    # Set security protocol to TLS 1.2 to avoid TLS errors
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    # Google API Authozation
    $scope = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file"
    $certPath = "C:\cert\treinresid-1733b1e86d36.p12"
    $iss = 'treinamentoresid@treinresid.iam.gserviceaccount.com'
    $certPswd = 'notasecret'
    $accessToken = Get-GOAuthTokenService -scope $scope -certPath $certPath -certPswd $certPswd -iss $iss

    $accessToken

    the return is still the same:

    Invoke-RestMethod : The remote server returned an error: (400) Bad Request
    No C:\Program Files\WindowsPowerShell\Modules\UMN-Google\1.2.12\UMN-Google.psm1:196 caractere:29
    + ... $response = Invoke-RestMethod -Uri "https://www.googleapis.com/oauth2 ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

    Only if there is some incompatibility.

    Thanks James, I’m going to do more research to see if I can find something.

    1. Avatar for William

      I’m getting the same. Interested if you find a fix!

      Invoke-RestMethod : The remote server returned an error: (400) Bad Request.
      At C:\Program Files\WindowsPowerShell\Modules\UMN-Google\1.2.12\UMN-Google.psm1:1594 char:9
      + Invoke-RestMethod -Method $method -Uri $uri -Body $json -Cont ...
      + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
      + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

      1. Avatar for Seth

        Go into that directory (C:\Program Files\WindowsPowerShell\Modules\UMN-Google\1.2.12\), and edit the file Google.psm1.

        On lines 196 and 1594, add -SkipHttpErrorCheck directly after Invoke-RestMethod. Should be:

        196: $response = Invoke-RestMethod -SkipHttpErrorCheck -Uri “https://www.googleapis.com/oauth2/v4/token” -Method Post -Body $fields -ContentType “application/x-www-form-urlencoded”

        1594: Invoke-RestMethod -SkipHttpErrorCheck -Method $method -Uri $uri -Body $json -ContentType “application/json” -Headers @{“Authorization”=”Bearer $accessToken”}

        This fixed it for me.

  7. Avatar for Andrew

    Hi, my return to get access token is:

    Invoke-RestMethod : O servidor remoto retornou um erro: (400) Solicitação Incorreta.
    No C:\Program Files\WindowsPowerShell\Modules\UMN-Google\1.2.12\UMN-Google.psm1:196 caractere:29
    + ... $response = Invoke-RestMethod -Uri "https://www.googleapis.com/oauth2 ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand

    Can you help me ?

    1. Avatar for James A. Chambers

      Hey Andrew,

      It looks like this is the authentication method failing. Did you download your cert and put the right file name in the script?

      If you did it could be a HTTPS error. You can force it to trust the cert using this method.

      Basically you’d add this to the top of the script:

      add-type @"
      using System.Net;
      using System.Security.Cryptography.X509Certificates;
      public class TrustAllCertsPolicy : ICertificatePolicy {
      public bool CheckValidationResult(
      ServicePoint srvPoint, X509Certificate certificate,
      WebRequest request, int certificateProblem) {
      return true;
      }
      }
      "@
      [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy

      Do you happen to know which line it was failing on? It seems like it’s the first and it’s not authenticating properly. Definitely double check all your authentication files and that it’s able to find them. I’ve had to use absolute paths to certificates in the past is another thing to try (C:/CertLocation/whatever instead of just “mycertificate”).

      Definitely let us know!

Leave a Comment

Your email address will not be published. Required fields are marked *

Type here..

Exit mobile version