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"); + } + } +}