From be0aabcf722e57896467a2424776cadd968988e9 Mon Sep 17 00:00:00 2001 From: matnun-br Date: Tue, 3 Nov 2026 12:46:20 +1100 Subject: [PATCH 1/5] Update project to query a new access token using the client assertion mechanism rather than relying on a static value which expires. All parameters are provided via environment variables --- .github/workflows/ci.yml | 4 +++- ShopifyNet.Tests/ShopifyClientTests.cs | 11 +++++++-- ShopifyNet.Tests/TestHelper.cs | 16 ++++++++++--- .../TokenBucketInterceptorTests.cs | 23 ++++++++----------- ShopifyNet.TypeGenerator/TypeGenerator.cs | 15 ++++++++---- 5 files changed, 46 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2e9224..69937b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,9 @@ jobs: - name: Test env: SHOPIFYNET_SHOP_ID: ${{ secrets.SHOPIFYNET_SHOP_ID }} - SHOPIFYNET_SHOP_TOKEN: ${{ secrets.SHOPIFYNET_SHOP_TOKEN }} + SHOPIFYNET_CLIENT_ID: ${{ secrets.SHOPIFYNET_CLIENT_ID }} + SHOPIFYNET_KEY_ID: ${{ secrets.SHOPIFYNET_KEY_ID }} + SHOPIFYNET_PRIVATE_KEY: ${{ secrets.SHOPIFYNET_PRIVATE_KEY }} run: dotnet test --no-restore --verbosity normal - name: Publish to nuget diff --git a/ShopifyNet.Tests/ShopifyClientTests.cs b/ShopifyNet.Tests/ShopifyClientTests.cs index ce7099a..bd618cc 100644 --- a/ShopifyNet.Tests/ShopifyClientTests.cs +++ b/ShopifyNet.Tests/ShopifyClientTests.cs @@ -8,8 +8,15 @@ namespace ShopifyNet.Tests; [TestClass] public class ShopifyClientTests { + private static string _accessToken; private ShopifyClient _client; + [ClassInitialize] + public static async Task InitializeClass(TestContext context) + { + _accessToken = await TestHelper.GetAccessTokenAsync(); + } + [TestInitialize] public void Initialize() { @@ -18,7 +25,7 @@ public void Initialize() private static ShopifyClientOptions GetClientOptions(bool throwOnGraphQLErrors = true, bool requestDetailedQueryCost = false) { - return new ShopifyClientOptions(TestHelper.ShopId, TestHelper.Token) + return new ShopifyClientOptions(TestHelper.ShopId, _accessToken) { Interceptor = NoOpInterceptor.Instance, ThrowOnGraphQLErrors = throwOnGraphQLErrors, @@ -301,4 +308,4 @@ SYNTAX ERROR!!! Assert.IsNotNull(response.errors); Assert.IsTrue(response.errors.Count > 0); } -} \ No newline at end of file +} diff --git a/ShopifyNet.Tests/TestHelper.cs b/ShopifyNet.Tests/TestHelper.cs index e109d08..36daff5 100644 --- a/ShopifyNet.Tests/TestHelper.cs +++ b/ShopifyNet.Tests/TestHelper.cs @@ -4,6 +4,16 @@ public class TestHelper { public static string ShopId => Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_ID", EnvironmentVariableTarget.User) ?? Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_ID"); - public static string Token => Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_TOKEN", EnvironmentVariableTarget.User) ?? - Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_TOKEN"); -} \ No newline at end of file + public static string ClientId => Environment.GetEnvironmentVariable("SHOPIFYNET_CLIENT_ID", EnvironmentVariableTarget.User) ?? + Environment.GetEnvironmentVariable("SHOPIFYNET_CLIENT_ID"); + public static string PrivateKey => Environment.GetEnvironmentVariable("SHOPIFYNET_PRIVATE_KEY", EnvironmentVariableTarget.User) ?? + Environment.GetEnvironmentVariable("SHOPIFYNET_PRIVATE_KEY"); + public static string KeyId => Environment.GetEnvironmentVariable("SHOPIFYNET_KEY_ID", EnvironmentVariableTarget.User) ?? + Environment.GetEnvironmentVariable("SHOPIFYNET_KEY_ID"); + public static async Task GetAccessTokenAsync() + { + var oauthClient = new ShopifyOAuthClient(ClientId, clientSecret: string.Empty); + var token = await oauthClient.RequestClientAssertionAccessTokenAsync(ShopId, KeyId, PrivateKey); + return token.access_token; + } +} diff --git a/ShopifyNet.Tests/TokenBucket/TokenBucketInterceptorTests.cs b/ShopifyNet.Tests/TokenBucket/TokenBucketInterceptorTests.cs index 9136b9c..7dcf666 100644 --- a/ShopifyNet.Tests/TokenBucket/TokenBucketInterceptorTests.cs +++ b/ShopifyNet.Tests/TokenBucket/TokenBucketInterceptorTests.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Http.Json; using System.Text.Json; using GraphQLSharp; @@ -14,21 +14,18 @@ public class TokenBucketInterceptorTests private ShopifyClientOptions CreateClientOptions(HttpStatusCode status, object response, IInterceptor interceptor, bool throwOnGraphQLErrors = true) { - return new ShopifyClientOptions(TestHelper.ShopId, TestHelper.Token) + var httpClient = new TestHttpClient(_ => new HttpResponseMessage { - Interceptor = interceptor, - ThrowOnGraphQLErrors = throwOnGraphQLErrors, - HttpClient = new TestHttpClient(msg => new HttpResponseMessage - { - StatusCode = status, - Content = JsonContent.Create(response) - }) - }; + StatusCode = status, + Content = JsonContent.Create(response) + }); + + return CreateClientOptions(httpClient, interceptor, throwOnGraphQLErrors); } private ShopifyClientOptions CreateClientOptions(HttpClient httpClient, IInterceptor interceptor, bool throwOnGraphQLErrors = true) { - return new ShopifyClientOptions(TestHelper.ShopId, TestHelper.Token) + return new ShopifyClientOptions("test.myshopify.com", "test-token") { Interceptor = interceptor, ThrowOnGraphQLErrors = throwOnGraphQLErrors, @@ -270,7 +267,7 @@ public async Task ShouldProcessInPriorityOrder() int cost = 962; int priority = 0; var interceptor = new TokenBucketInterceptor(r => priority); - var options = new ShopifyClientOptions(TestHelper.ShopId, TestHelper.Token) + var options = new ShopifyClientOptions(TestHelper.ShopId, await TestHelper.GetAccessTokenAsync()) { Interceptor = interceptor, }; @@ -292,4 +289,4 @@ public async Task ShouldProcessInPriorityOrder() var responsesByPriority = responses.OrderBy(i => i.reqPriority).ToArray(); Assert.IsTrue(responsesByPriority.SequenceEqual(responsesByElapsed)); } -} \ No newline at end of file +} diff --git a/ShopifyNet.TypeGenerator/TypeGenerator.cs b/ShopifyNet.TypeGenerator/TypeGenerator.cs index 8888108..ceaa01c 100644 --- a/ShopifyNet.TypeGenerator/TypeGenerator.cs +++ b/ShopifyNet.TypeGenerator/TypeGenerator.cs @@ -40,10 +40,12 @@ var generator = new GraphQLTypeGenerator(); string csharpCode = await generator.GenerateTypesAsync(options, async query => { - string shopId = Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_ID", EnvironmentVariableTarget.User)!; - string token = Environment.GetEnvironmentVariable("SHOPIFYNET_SHOP_TOKEN", EnvironmentVariableTarget.User)!; + string shopId = GetRequiredEnvironmentVariable("SHOPIFYNET_SHOP_ID"); + var oauthClient = new ShopifyOAuthClient(GetRequiredEnvironmentVariable("SHOPIFYNET_CLIENT_ID"), clientSecret: string.Empty); + var token = await oauthClient.RequestClientAssertionAccessTokenAsync(shopId, + GetRequiredEnvironmentVariable("SHOPIFYNET_KEY_ID"), GetRequiredEnvironmentVariable("SHOPIFYNET_PRIVATE_KEY")); - var shopifyOptions = new ShopifyClientOptions(shopId, token) as IGraphQLClientOptions; + var shopifyOptions = new ShopifyClientOptions(shopId, token.access_token) as IGraphQLClientOptions; var options = new GraphQLClientOptions(shopifyOptions.Uri) { @@ -57,4 +59,9 @@ File.WriteAllText($@"{GetDir()}/../ShopifyNet/Generated.cs", csharpCode); -static string GetDir([CallerFilePath] string filePath = "") => Path.GetDirectoryName(filePath)!; \ No newline at end of file +static string GetDir([CallerFilePath] string filePath = "") => Path.GetDirectoryName(filePath)!; + +static string GetRequiredEnvironmentVariable(string name) => + Environment.GetEnvironmentVariable(name) ?? + Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.User) ?? + throw new InvalidOperationException($"Set {name} before running the type generator."); From 6fb081ec6a5ba22a592d742734fe4006a1fa23ea Mon Sep 17 00:00:00 2001 From: matnun-br Date: Tue, 3 Nov 2026 12:58:21 +1100 Subject: [PATCH 2/5] Fix test error --- .../ShopifyClientAssertionAccessTokenRequestTests.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ShopifyNet.Tests/ShopifyClientAssertionAccessTokenRequestTests.cs b/ShopifyNet.Tests/ShopifyClientAssertionAccessTokenRequestTests.cs index 02dec24..5729747 100644 --- a/ShopifyNet.Tests/ShopifyClientAssertionAccessTokenRequestTests.cs +++ b/ShopifyNet.Tests/ShopifyClientAssertionAccessTokenRequestTests.cs @@ -95,7 +95,15 @@ public async Task RejectsPublicOnlyKeysBeforeSending(string algorithm) using AsymmetricAlgorithm key = algorithm == "ES256" ? ECDsa.Create(ECCurve.NamedCurves.nistP256) : RSA.Create(2048); using var http = new TestHttpClient(_ => throw new AssertFailedException("Public-only key must not be sent.")); var client = new ShopifyOAuthClient("client-id", string.Empty, http); - await Assert.ThrowsExceptionAsync(() => client.RequestClientAssertionAccessTokenAsync( - "test.myshopify.com", "key", key.ExportSubjectPublicKeyInfoPem())); + try + { + await client.RequestClientAssertionAccessTokenAsync( + "test.myshopify.com", "key", key.ExportSubjectPublicKeyInfoPem()); + Assert.Fail("A public-only key must fail signing."); + } + catch (CryptographicException) + { + // Accept platform-specific subclasses, including OpenSSL exceptions on Linux. + } } } From f374dbf9d45a3bcd34c4b9d5ba5b4abaee6f7eb8 Mon Sep 17 00:00:00 2001 From: Clement Gutel Date: Tue, 3 Nov 2026 13:17:13 +1100 Subject: [PATCH 3/5] code review changes --- ShopifyNet.Tests/TestHelper.cs | 2 +- ShopifyNet.TypeGenerator/TypeGenerator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ShopifyNet.Tests/TestHelper.cs b/ShopifyNet.Tests/TestHelper.cs index 36daff5..bf785f4 100644 --- a/ShopifyNet.Tests/TestHelper.cs +++ b/ShopifyNet.Tests/TestHelper.cs @@ -12,7 +12,7 @@ public class TestHelper Environment.GetEnvironmentVariable("SHOPIFYNET_KEY_ID"); public static async Task GetAccessTokenAsync() { - var oauthClient = new ShopifyOAuthClient(ClientId, clientSecret: string.Empty); + var oauthClient = new ShopifyOAuthClient(ClientId, null); var token = await oauthClient.RequestClientAssertionAccessTokenAsync(ShopId, KeyId, PrivateKey); return token.access_token; } diff --git a/ShopifyNet.TypeGenerator/TypeGenerator.cs b/ShopifyNet.TypeGenerator/TypeGenerator.cs index ceaa01c..1be8841 100644 --- a/ShopifyNet.TypeGenerator/TypeGenerator.cs +++ b/ShopifyNet.TypeGenerator/TypeGenerator.cs @@ -41,7 +41,7 @@ string csharpCode = await generator.GenerateTypesAsync(options, async query => { string shopId = GetRequiredEnvironmentVariable("SHOPIFYNET_SHOP_ID"); - var oauthClient = new ShopifyOAuthClient(GetRequiredEnvironmentVariable("SHOPIFYNET_CLIENT_ID"), clientSecret: string.Empty); + var oauthClient = new ShopifyOAuthClient(GetRequiredEnvironmentVariable("SHOPIFYNET_CLIENT_ID"), null); var token = await oauthClient.RequestClientAssertionAccessTokenAsync(shopId, GetRequiredEnvironmentVariable("SHOPIFYNET_KEY_ID"), GetRequiredEnvironmentVariable("SHOPIFYNET_PRIVATE_KEY")); From 7c66c0e987b5ad952a80f527d81069d25dd085b1 Mon Sep 17 00:00:00 2001 From: Clement Gutel Date: Tue, 3 Nov 2026 13:18:01 +1100 Subject: [PATCH 4/5] Bump version to 2.20.0 in project file --- ShopifyNet/ShopifyNet.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShopifyNet/ShopifyNet.csproj b/ShopifyNet/ShopifyNet.csproj index 8a27710..932392f 100644 --- a/ShopifyNet/ShopifyNet.csproj +++ b/ShopifyNet/ShopifyNet.csproj @@ -5,7 +5,7 @@ https://github.com/Wish-Org/ShopifyNet https://github.com/Wish-Org/ShopifyNet .NET utilities for Shopify GraphQL - 2.19.0 + 2.20.0 shopify graphql types classes schema generator strongly typed From 51a746b894da6ac2c44de3b88e10caa46ae23349 Mon Sep 17 00:00:00 2001 From: matnun-br Date: Tue, 3 Nov 2026 14:04:51 +1100 Subject: [PATCH 5/5] Client secret can be null in certain flows. Move null check where necessary --- ShopifyNet/OAuth/ShopifyOAuthClient.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ShopifyNet/OAuth/ShopifyOAuthClient.cs b/ShopifyNet/OAuth/ShopifyOAuthClient.cs index 6368390..82940e8 100644 --- a/ShopifyNet/OAuth/ShopifyOAuthClient.cs +++ b/ShopifyNet/OAuth/ShopifyOAuthClient.cs @@ -53,7 +53,8 @@ public class ShopifyOAuthClient public ShopifyOAuthClient(string clientId, string clientSecret, HttpClient httpClient = null) { _clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); - _clientSecret = clientSecret ?? throw new ArgumentNullException(nameof(clientSecret)); + // The client secret can be null for certain flows, such as client assertion. + _clientSecret = clientSecret; _httpClient = httpClient ?? _defaultHttpClient; } @@ -113,6 +114,9 @@ public static bool IsValidShopDomain(string myShopifyDomain) /// public bool IsValidHmac(IEnumerable> queryParameters) { + if (_clientSecret == null) + throw new ArgumentNullException(nameof(_clientSecret)); + if (queryParameters == null) return false; @@ -389,7 +393,11 @@ private async Task RequestAccessTokenAsync(string myShopifyDomain, Diction parameters[CLIENT_ID_KEY] = _clientId; if (includeClientSecret) + { + if (_clientSecret == null) + throw new ArgumentNullException(nameof(_clientSecret)); parameters[CLIENT_SECRET_KEY] = _clientSecret; + } using var request = new HttpRequestMessage(HttpMethod.Post, $"https://{myShopifyDomain}/admin/oauth/access_token") {