Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f3e8f0e
Add local claude settings to gitignore
PearlAegis Sep 1, 2025
3310f44
Add BouncyCastle.Cryptography dependency for Ed25519 cryptography
PearlAegis Sep 1, 2025
7dcee70
Add validation error messages for SecretApiKey authentication
PearlAegis Sep 1, 2025
27d53ff
Add SecretApiKey configuration models
PearlAegis Sep 1, 2025
414659d
Implement SecretApiKey JWT authentication using Ed25519
PearlAegis Sep 1, 2025
d74d72a
Add test helpers for SecretApiKey authentication
PearlAegis Sep 1, 2025
0d3c28e
Add unit tests for SecretApiKey authentication
PearlAegis Sep 1, 2025
133c04c
Add Authorization header constant for JWT Bearer authentication
PearlAegis Sep 1, 2025
885be53
Add Bearer token format string resource
PearlAegis Sep 1, 2025
5790f45
Replace deprecated ApiKeyAuthenticator with SecretApiKeyAuthenticator
PearlAegis Sep 1, 2025
046ef4f
Mark legacy configuration models as obsolete
PearlAegis Sep 1, 2025
009005b
Update unit tests to use new SecretApiKey authentication
PearlAegis Sep 1, 2025
1e646b8
Switch to ES256 key format.
PearlAegis Sep 1, 2025
5a4fa8a
Fix unit tests
PearlAegis Sep 1, 2025
ab48784
Update WebSocket SubscriptionMessage
PearlAegis Sep 2, 2025
e8d40d7
Merge config files into CoinbaseClientConfig.
PearlAegis Sep 2, 2025
889072a
Add configuration extension
PearlAegis Sep 2, 2025
c2a0a19
Move IDisposable to websocket interface
PearlAegis Sep 2, 2025
91f4d14
Change registration extension
PearlAegis Sep 2, 2025
8625870
Use IOptions in api and websocket constructors
PearlAegis Sep 2, 2025
d78d663
Simplify client configuration extension
PearlAegis Sep 9, 2025
d994034
Update changelog and readme
PearlAegis Sep 9, 2025
c3203d9
Fix formatting in unit test.
PearlAegis Sep 9, 2025
92963e9
Throw ArgumentException consistently in SecretApiKeyAuthenticator.
PearlAegis Sep 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
727 changes: 364 additions & 363 deletions .gitignore

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.0] - 2025-09-08

### Added
- JWT Bearer Authentication with ES256 signatures replacing legacy HMAC
- Unified `CoinbaseClientConfig` for both API and WebSocket clients
- Dependency Injection support with `AddCoinbaseAdvancedTradeClient()` extension
- Enhanced security with JWT tokens (2-minute expiration and replay protection)

### Changed
- Migrated from HMAC-SHA256 to JWT Bearer tokens with ECDSA ES256 signatures
- Configuration properties renamed: `ApiKey`/`ApiSecret` → `KeyName`/`KeySecret`
- Requires Coinbase Cloud API keys (EC P-256 private keys in PEM format)
- Constructor parameters now use `IOptions<CoinbaseClientConfig>`

### Removed
- Legacy `ApiKeyAuthenticator` and CB-ACCESS-* header authentication

## [0.3.0] - 2025-08-25

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using CoinbaseAdvancedTradeClient.Authentication;
using CoinbaseAdvancedTradeClient.UnitTests.TestHelpers;
using Xunit;

namespace CoinbaseAdvancedTradeClient.UnitTests.Authentication
{
public class SecretApiKeyAuthenticatorTests
{
[Fact]
public void GenerateBearerJWT_ValidParameters_ReturnsJWT()
{
// Arrange
var testKeySecret = TestConfigHelper.GenerateTestKeySecret();

// Act
var result = SecretApiKeyAuthenticator.GenerateBearerJWT(
"test-key-name",
testKeySecret,
"GET",
"api.coinbase.com",
"/v1/test"
);

// Assert
Assert.NotNull(result);
Assert.Contains(".", result);
var parts = result.Split('.');
Assert.Equal(3, parts.Length); // header.payload.signature
}

[Fact]
public void GenerateBearerJWT_InvalidKeySecret_ThrowsArgumentException()
{
// Act & Assert
Assert.Throws<ArgumentException>(() =>
{
SecretApiKeyAuthenticator.GenerateBearerJWT(
"test-key-name",
"invalid-key-format",
"GET",
"api.coinbase.com",
"/v1/test"
);
});
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CoinbaseAdvancedTradeClient.Models.Config;
using Microsoft.Extensions.Options;
using Xunit;

namespace CoinbaseAdvancedTradeClient.UnitTests
Expand All @@ -9,7 +10,7 @@ public class CoinbaseAdvancedTradeApiClientTests
public void Constructor_NullConfig_ThrowsArgumentNullException()
{
//Arrange
ApiClientConfig config = null;
IOptions<CoinbaseClientConfig> config = null;

//Act & Assert
Assert.Throws<ArgumentNullException>(() =>
Expand All @@ -27,11 +28,12 @@ public void Constructor_NullConfig_ThrowsArgumentNullException()
public void Constructor_EmptyConfigSetting_ThrowsArgumentException(string key, string secret)
{
//Arrange
ApiClientConfig config = new ApiClientConfig()
var configValue = new CoinbaseClientConfig()
{
ApiKey = key,
ApiSecret = secret
KeyName = key,
KeySecret = secret
};
var config = Options.Create(configValue);

//Act & Assert
Assert.Throws<ArgumentException>(() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="8.3.0" />
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using CoinbaseAdvancedTradeClient.Constants;
using CoinbaseAdvancedTradeClient.Interfaces;
using CoinbaseAdvancedTradeClient.Models.Config;
using Microsoft.Extensions.Options;
using System.Net.Sockets;
using Xunit;

Expand All @@ -12,7 +13,7 @@ public class CoinbaseAdvancedTradeWebSocketClientTests
public void Constructor_NullConfig_ThrowsArgumentNullException()
{
//Arrange
WebSocketClientConfig config = null;
IOptions<CoinbaseClientConfig> config = null;

//Act & Assert
Assert.Throws<ArgumentNullException>(() =>
Expand All @@ -30,11 +31,12 @@ public void Constructor_NullConfig_ThrowsArgumentNullException()
public void Constructor_EmptyConfigSetting_ThrowsArgumentException(string key, string secret)
{
//Arrange
WebSocketClientConfig config = new WebSocketClientConfig()
var configValue = new CoinbaseClientConfig()
{
ApiKey = key,
ApiSecret = secret
KeyName = key,
KeySecret = secret
};
var config = Options.Create(configValue);

//Act & Assert
Assert.Throws<ArgumentException>(() =>
Expand Down Expand Up @@ -164,11 +166,12 @@ public void Unsubscribe_EmptyProductIds_ThrowsArgumentNullException()

private ICoinbaseAdvancedTradeWebSocketClient CreateTestClient()
{
WebSocketClientConfig config = new WebSocketClientConfig()
var configValue = new CoinbaseClientConfig()
{
ApiKey = "testKey",
ApiSecret = "testSecret"
KeyName = "testKey",
KeySecret = TestHelpers.TestConfigHelper.GenerateTestKeySecret()
};
var config = Options.Create(configValue);

return new CoinbaseAdvancedTradeWebSocketClient(config);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using CoinbaseAdvancedTradeClient.Models.Pages;
using Flurl.Http;
using Flurl.Http.Testing;
using Microsoft.Extensions.Options;
using System.Globalization;
using Xunit;

Expand All @@ -16,11 +17,12 @@ public class AccountsEndpointTests

public AccountsEndpointTests()
{
var config = new ApiClientConfig()
var configValue = new CoinbaseClientConfig()
{
ApiKey = "key",
ApiSecret = "secret"
KeyName = "key",
KeySecret = TestHelpers.TestConfigHelper.GenerateTestKeySecret()
};
var config = Options.Create(configValue);

_testClient = new CoinbaseAdvancedTradeApiClient(config);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using FakeItEasy;
using Flurl.Http;
using Flurl.Http.Testing;
using Microsoft.Extensions.Options;
using Xunit;

namespace CoinbaseAdvancedTradeClient.UnitTests.Endpoints
Expand All @@ -17,11 +18,12 @@ public class OrdersEndpointTests

public OrdersEndpointTests()
{
var config = new ApiClientConfig()
var configValue = new CoinbaseClientConfig()
{
ApiKey = "key",
ApiSecret = "secret"
KeyName = "key",
KeySecret = TestHelpers.TestConfigHelper.GenerateTestKeySecret()
};
var config = Options.Create(configValue);

_testClient = new CoinbaseAdvancedTradeApiClient(config);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using CoinbaseAdvancedTradeClient.Resources;
using Flurl.Http;
using Flurl.Http.Testing;
using Microsoft.Extensions.Options;
using Xunit;

namespace CoinbaseAdvancedTradeClient.UnitTests.Endpoints
Expand All @@ -17,11 +18,12 @@ public class ProductsEndpointTests

public ProductsEndpointTests()
{
var config = new ApiClientConfig()
var configValue = new CoinbaseClientConfig()
{
ApiKey = "key",
ApiSecret = "secret"
KeyName = "key",
KeySecret = TestHelpers.TestConfigHelper.GenerateTestKeySecret()
};
var config = Options.Create(configValue);

_testClient = new CoinbaseAdvancedTradeApiClient(config);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using CoinbaseAdvancedTradeClient.Models.Config;
using Flurl.Http;
using Flurl.Http.Testing;
using Microsoft.Extensions.Options;
using Xunit;

namespace CoinbaseAdvancedTradeClient.UnitTests.Endpoints
Expand All @@ -16,11 +17,12 @@ public class TransactionSummaryEndpointTests

public TransactionSummaryEndpointTests()
{
var config = new ApiClientConfig()
var configValue = new CoinbaseClientConfig()
{
ApiKey = "key",
ApiSecret = "secret"
KeyName = "key",
KeySecret = TestHelpers.TestConfigHelper.GenerateTestKeySecret()
};
var config = Options.Create(configValue);

_testClient = new CoinbaseAdvancedTradeApiClient(config);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Org.BouncyCastle.Asn1.Sec;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;

namespace CoinbaseAdvancedTradeClient.UnitTests.TestHelpers
{
public static class TestConfigHelper
{
// Generate a valid EC P-256 private key for testing purposes
public static string GenerateTestKeySecret()
{
// Generate P-256 (secp256r1) key pair
var keyGen = new ECKeyPairGenerator();
var curveParams = SecNamedCurves.GetByName("secp256r1");
var domainParams = new ECDomainParameters(curveParams.Curve, curveParams.G, curveParams.N, curveParams.H);
keyGen.Init(new ECKeyGenerationParameters(domainParams, new SecureRandom()));

var keyPair = keyGen.GenerateKeyPair();
var privateKey = (ECPrivateKeyParameters)keyPair.Private;

// Convert to PEM format
using var stringWriter = new StringWriter();
var pemWriter = new PemWriter(stringWriter);
pemWriter.WriteObject(privateKey);

return stringWriter.ToString();
}
}
}

This file was deleted.

Loading
Loading