From 326f9b7f3f59b6a0dd13f03f48cf257ed035c6ac Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 17 Dec 2025 20:18:52 +0000
Subject: [PATCH 1/4] Initial plan
From 7f946ffe82bc5ff063bb769f148fb62abf4dc96e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 17 Dec 2025 20:25:11 +0000
Subject: [PATCH 2/4] Add OAuth and alternative authentication support to
GoogleSheetsWrapper
Co-authored-by: SteveWinward <2002602+SteveWinward@users.noreply.github.com>
---
src/GoogleSheetsWrapper/SheetAppender.cs | 23 +++++++++-
src/GoogleSheetsWrapper/SheetExporter.cs | 23 +++++++++-
src/GoogleSheetsWrapper/SheetHelper.cs | 53 ++++++++++++++++++++++--
3 files changed, 92 insertions(+), 7 deletions(-)
diff --git a/src/GoogleSheetsWrapper/SheetAppender.cs b/src/GoogleSheetsWrapper/SheetAppender.cs
index e89ebe0..57f0c72 100644
--- a/src/GoogleSheetsWrapper/SheetAppender.cs
+++ b/src/GoogleSheetsWrapper/SheetAppender.cs
@@ -18,7 +18,7 @@ public class SheetAppender
private readonly SheetHelper _sheetHelper;
///
- /// Constructor
+ /// Constructor for use with service account authentication
///
///
///
@@ -28,6 +28,16 @@ public SheetAppender(string spreadsheetID, string serviceAccountEmail, string ta
_sheetHelper = new SheetHelper(spreadsheetID, serviceAccountEmail, tabName);
}
+ ///
+ /// Constructor for use with OAuth or other credential types
+ ///
+ ///
+ ///
+ public SheetAppender(string spreadsheetID, string tabName)
+ {
+ _sheetHelper = new SheetHelper(spreadsheetID, tabName);
+ }
+
///
/// Constructor
///
@@ -38,7 +48,7 @@ public SheetAppender(SheetHelper sheetHelper)
}
///
- /// Initializes the SheetAppender object with authentication to Google Sheets API
+ /// Initializes the SheetAppender object with service account authentication to Google Sheets API
///
///
public void Init(string jsonCredentials)
@@ -46,6 +56,15 @@ public void Init(string jsonCredentials)
_sheetHelper.Init(jsonCredentials);
}
+ ///
+ /// Initializes the SheetAppender object with a credential (supports OAuth user credentials, service account credentials, etc.)
+ ///
+ /// Google credential (e.g., UserCredential from OAuth, ServiceAccountCredential, etc.)
+ public void Init(Google.Apis.Auth.OAuth2.ICredential credential)
+ {
+ _sheetHelper.Init(credential);
+ }
+
///
/// Appends a CSV file and all its rows into the current Google Sheets tab
///
diff --git a/src/GoogleSheetsWrapper/SheetExporter.cs b/src/GoogleSheetsWrapper/SheetExporter.cs
index 17d2598..baef9cd 100644
--- a/src/GoogleSheetsWrapper/SheetExporter.cs
+++ b/src/GoogleSheetsWrapper/SheetExporter.cs
@@ -16,7 +16,7 @@ public class SheetExporter
private readonly SheetHelper _sheetHelper;
///
- /// Constructor
+ /// Constructor for use with service account authentication
///
///
///
@@ -26,6 +26,16 @@ public SheetExporter(string spreadsheetID, string serviceAccountEmail, string ta
_sheetHelper = new SheetHelper(spreadsheetID, serviceAccountEmail, tabName);
}
+ ///
+ /// Constructor for use with OAuth or other credential types
+ ///
+ ///
+ ///
+ public SheetExporter(string spreadsheetID, string tabName)
+ {
+ _sheetHelper = new SheetHelper(spreadsheetID, tabName);
+ }
+
///
/// Constructor
///
@@ -36,7 +46,7 @@ public SheetExporter(SheetHelper sheetHelper)
}
///
- ///
+ /// Initializes the SheetExporter object with service account authentication to Google Sheets API
///
///
public void Init(string jsonCredentials)
@@ -44,6 +54,15 @@ public void Init(string jsonCredentials)
_sheetHelper.Init(jsonCredentials);
}
+ ///
+ /// Initializes the SheetExporter object with a credential (supports OAuth user credentials, service account credentials, etc.)
+ ///
+ /// Google credential (e.g., UserCredential from OAuth, ServiceAccountCredential, etc.)
+ public void Init(Google.Apis.Auth.OAuth2.ICredential credential)
+ {
+ _sheetHelper.Init(credential);
+ }
+
///
/// Exports the current Google Sheet tab to a CSV file
///
diff --git a/src/GoogleSheetsWrapper/SheetHelper.cs b/src/GoogleSheetsWrapper/SheetHelper.cs
index 4df669d..4da702e 100644
--- a/src/GoogleSheetsWrapper/SheetHelper.cs
+++ b/src/GoogleSheetsWrapper/SheetHelper.cs
@@ -53,7 +53,7 @@ public class SheetHelper
private bool IsInitialized;
///
- /// Constructor
+ /// Constructor for use with service account authentication
///
///
///
@@ -65,6 +65,17 @@ public SheetHelper(string spreadsheetID, string serviceAccountEmail, string tabN
TabName = tabName;
}
+ ///
+ /// Constructor for use with OAuth or other credential types that don't require a service account email
+ ///
+ ///
+ ///
+ public SheetHelper(string spreadsheetID, string tabName)
+ {
+ SpreadsheetID = spreadsheetID;
+ TabName = tabName;
+ }
+
///
/// Initializes the SheetHelper object
///
@@ -106,6 +117,34 @@ public void Init(string jsonCredentials, Google.Apis.Http.IHttpClientFactory htt
UpdateTabName(TabName);
}
+ ///
+ /// Initializes the SheetHelper object with a credential (supports OAuth user credentials, service account credentials, etc.)
+ ///
+ /// Google credential (e.g., UserCredential from OAuth, ServiceAccountCredential, etc.)
+ public void Init(ICredential credential)
+ {
+ Init(credential, default);
+ }
+
+ ///
+ /// Initializes the SheetHelper object with a credential and optional HTTP client factory
+ ///
+ /// Google credential (e.g., UserCredential from OAuth, ServiceAccountCredential, etc.)
+ /// Optional HTTP client factory
+ public void Init(ICredential credential, Google.Apis.Http.IHttpClientFactory httpClientFactory)
+ {
+ var service = new SheetsService(new BaseClientService.Initializer()
+ {
+ HttpClientInitializer = credential,
+ HttpClientFactory = httpClientFactory,
+ });
+
+ Service = service;
+ IsInitialized = true;
+
+ UpdateTabName(TabName);
+ }
+
///
/// Throws ArgumentException if the Init() method has not been called yet.
///
@@ -114,7 +153,7 @@ protected void EnsureServiceInitialized()
{
if (!IsInitialized)
{
- throw new ArgumentException("SheetHelper requires the Init(string jsonCredentials) method to be called before using any of its methods.");
+ throw new ArgumentException("SheetHelper requires an Init() method to be called before using any of its methods.");
}
}
@@ -550,7 +589,7 @@ private BatchUpdateSpreadsheetResponse CreateNewTab(string newTabName)
public class SheetHelper : SheetHelper where T : BaseRecord
{
///
- /// Constructor
+ /// Constructor for use with service account authentication
///
///
///
@@ -558,6 +597,14 @@ public class SheetHelper : SheetHelper where T : BaseRecord
public SheetHelper(string spreadsheetID, string serviceAccountEmail, string tabName)
: base(spreadsheetID, serviceAccountEmail, tabName) { }
+ ///
+ /// Constructor for use with OAuth or other credential types that don't require a service account email
+ ///
+ ///
+ ///
+ public SheetHelper(string spreadsheetID, string tabName)
+ : base(spreadsheetID, tabName) { }
+
///
/// Adds a record to the next row in the Google Sheet tab
///
From 4ff64230812cebe0febe8348e8bef16b969a9982 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 17 Dec 2025 20:26:26 +0000
Subject: [PATCH 3/4] Update README with OAuth authentication documentation and
examples
Co-authored-by: SteveWinward <2002602+SteveWinward@users.noreply.github.com>
---
README.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 81 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 0b02c03..40df680 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,11 @@
## Google Sheets API .NET Wrapper Library
> [!IMPORTANT]
-> Using this library requires you to use a Service Account to access your Google Sheets spreadsheets. Please review the authentication section farther down for more details on how to set this up.
+> This library supports two authentication methods:
+> - **Service Account Authentication** - for server-to-server scenarios
+> - **OAuth 2.0 User Authentication** - for accessing organization Shared Drives and user-specific resources
+>
+> Please review the authentication section below for details on how to set up each method.
This library allows you to use strongly typed objects against a Google Sheets spreadsheet without having to have knowledge on the Google Sheets API methods and protocols.
@@ -478,8 +482,18 @@ using (var stream = new FileStream(filepath, FileMode.Create))
```
## Authentication
+
+This library supports two authentication methods:
+1. **Service Account Authentication** (recommended for server-to-server scenarios)
+2. **OAuth 2.0 User Authentication** (recommended for accessing organization Shared Drives)
+
+> [!NOTE]
+> Service accounts cannot access Shared Drives that are restricted to organization members. Use OAuth 2.0 authentication if you need to access organization Shared Drives.
+
+### Option 1: Service Account Authentication
+
> [!IMPORTANT]
-> You need to setup a Google API Service Account before you can use this library.
+> You need to setup a Google API Service Account before you can use this authentication method.
1. If you have not yet created a Google Cloud project, you will need to create one before you create a service account. Documentation on this can be found below,
@@ -520,3 +534,68 @@ sheetHelper.Init(settings.JsonCredential);
Another good article on how to setup a Google Service Account can be found below on Robocorp's documentation site,
https://robocorp.com/docs/development-guide/google-sheets/interacting-with-google-sheets#create-a-google-service-account
+
+### Option 2: OAuth 2.0 User Authentication
+
+OAuth 2.0 authentication allows users to authenticate with their Google account, which enables access to organization Shared Drives and resources restricted to organization members.
+
+1. Create OAuth 2.0 credentials in your Google Cloud project:
+
+ https://developers.google.com/workspace/guides/create-credentials#oauth-client-id
+
+2. Enable the ````Google Sheets API```` for your Google Cloud project (if not already enabled).
+
+3. Install the necessary NuGet package for OAuth authentication in your project:
+ ```bash
+ dotnet add package Google.Apis.Auth
+ ```
+
+4. Implement OAuth 2.0 flow and initialize ````SheetHelper```` with user credentials:
+
+```csharp
+using Google.Apis.Auth.OAuth2;
+using Google.Apis.Util.Store;
+using System.IO;
+using System.Threading;
+
+// Load client secrets from the OAuth credentials file you downloaded
+UserCredential credential;
+using (var stream = new FileStream("client_secrets.json", FileMode.Open, FileAccess.Read))
+{
+ string credPath = "token.json";
+ credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
+ GoogleClientSecrets.FromStream(stream).Secrets,
+ new[] { SheetsService.Scope.Spreadsheets },
+ "user",
+ CancellationToken.None,
+ new FileDataStore(credPath, true));
+}
+
+// Create a SheetHelper without service account email (not needed for OAuth)
+var sheetHelper = new SheetHelper(
+ settings.GoogleSpreadsheetId,
+ settings.GoogleMainSheetName);
+
+// Initialize with the OAuth credential
+sheetHelper.Init(credential);
+```
+
+You can also use OAuth credentials with ````SheetAppender```` and ````SheetExporter````:
+
+```csharp
+// Using SheetAppender with OAuth
+var appender = new SheetAppender(
+ settings.GoogleSpreadsheetId,
+ settings.GoogleMainSheetName);
+appender.Init(credential);
+
+// Using SheetExporter with OAuth
+var exporter = new SheetExporter(
+ settings.GoogleSpreadsheetId,
+ settings.GoogleMainSheetName);
+exporter.Init(credential);
+```
+
+For more information on OAuth 2.0 authentication with Google APIs:
+- https://developers.google.com/identity/protocols/oauth2
+- https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth
From 81f93dd7080e0b11ed60cefd2218a5c07813e350 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 17 Dec 2025 20:31:37 +0000
Subject: [PATCH 4/4] Add unit tests for new OAuth authentication constructors
and Init methods
Co-authored-by: SteveWinward <2002602+SteveWinward@users.noreply.github.com>
---
.../AuthenticationTests.cs | 100 ++++++++++++++++++
1 file changed, 100 insertions(+)
create mode 100644 src/GoogleSheetsWrapper.Tests/AuthenticationTests.cs
diff --git a/src/GoogleSheetsWrapper.Tests/AuthenticationTests.cs b/src/GoogleSheetsWrapper.Tests/AuthenticationTests.cs
new file mode 100644
index 000000000..27b1dbf
--- /dev/null
+++ b/src/GoogleSheetsWrapper.Tests/AuthenticationTests.cs
@@ -0,0 +1,100 @@
+using NUnit.Framework;
+using Google.Apis.Auth.OAuth2;
+
+namespace GoogleSheetsWrapper.Tests
+{
+ [TestFixture]
+ public class AuthenticationTests
+ {
+ [Test]
+ public void SheetHelperConstructorWithoutServiceAccountEmailDoesNotThrow()
+ {
+ // Arrange & Act
+ var sheetHelper = new SheetHelper("testSpreadsheetId", "testTab");
+
+ // Assert
+ Assert.That(sheetHelper, Is.Not.Null);
+ Assert.That(sheetHelper.SpreadsheetID, Is.EqualTo("testSpreadsheetId"));
+ Assert.That(sheetHelper.TabName, Is.EqualTo("testTab"));
+ }
+
+ [Test]
+ public void SheetHelperConstructorWithServiceAccountEmailDoesNotThrow()
+ {
+ // Arrange & Act
+ var sheetHelper = new SheetHelper("testSpreadsheetId", "service@account.com", "testTab");
+
+ // Assert
+ Assert.That(sheetHelper, Is.Not.Null);
+ Assert.That(sheetHelper.SpreadsheetID, Is.EqualTo("testSpreadsheetId"));
+ Assert.That(sheetHelper.ServiceAccountEmail, Is.EqualTo("service@account.com"));
+ Assert.That(sheetHelper.TabName, Is.EqualTo("testTab"));
+ }
+
+ [Test]
+ public void GenericSheetHelperConstructorWithoutServiceAccountEmailDoesNotThrow()
+ {
+ // Arrange & Act
+ var sheetHelper = new SheetHelper("testSpreadsheetId", "testTab");
+
+ // Assert
+ Assert.That(sheetHelper, Is.Not.Null);
+ Assert.That(sheetHelper.SpreadsheetID, Is.EqualTo("testSpreadsheetId"));
+ Assert.That(sheetHelper.TabName, Is.EqualTo("testTab"));
+ }
+
+ [Test]
+ public void SheetAppenderConstructorWithoutServiceAccountEmailDoesNotThrow()
+ {
+ // Arrange & Act
+ var appender = new SheetAppender("testSpreadsheetId", "testTab");
+
+ // Assert
+ Assert.That(appender, Is.Not.Null);
+ }
+
+ [Test]
+ public void SheetExporterConstructorWithoutServiceAccountEmailDoesNotThrow()
+ {
+ // Arrange & Act
+ var exporter = new SheetExporter("testSpreadsheetId", "testTab");
+
+ // Assert
+ Assert.That(exporter, Is.Not.Null);
+ }
+
+ [Test]
+ public void SheetHelperInitWithICredentialMethodExists()
+ {
+ // Arrange
+ var sheetHelper = new SheetHelper("testSpreadsheetId", "testTab");
+
+ // Act & Assert - verifies the method signature exists and accepts ICredential
+ // Note: We can't fully test Init without a real Google Sheets service
+ var initMethod = typeof(SheetHelper).GetMethod("Init", new[] { typeof(ICredential) });
+ Assert.That(initMethod, Is.Not.Null, "Init(ICredential) method should exist");
+ }
+
+ [Test]
+ public void SheetAppenderInitWithICredentialMethodExists()
+ {
+ // Arrange
+ var appender = new SheetAppender("testSpreadsheetId", "testTab");
+
+ // Act & Assert - verifies the method signature exists and accepts ICredential
+ var initMethod = typeof(SheetAppender).GetMethod("Init", new[] { typeof(ICredential) });
+ Assert.That(initMethod, Is.Not.Null, "Init(ICredential) method should exist");
+ }
+
+ [Test]
+ public void SheetExporterInitWithICredentialMethodExists()
+ {
+ // Arrange
+ var exporter = new SheetExporter("testSpreadsheetId", "testTab");
+
+ // Act & Assert - verifies the method signature exists and accepts ICredential
+ var initMethod = typeof(SheetExporter).GetMethod("Init", new[] { typeof(ICredential) });
+ Assert.That(initMethod, Is.Not.Null, "Init(ICredential) method should exist");
+ }
+ }
+}