Skip to content

Commit b44075e

Browse files
authored
implement new Coinbase JWT Bearer token for Authentication header (#857)
1 parent 09a30c9 commit b44075e

File tree

5 files changed

+141
-17
lines changed

5 files changed

+141
-17
lines changed

src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ The above copyright notice and this permission notice shall be included in all c
1616
using System.Collections.Generic;
1717
using System.Diagnostics;
1818
using System.Linq;
19+
using System.Net;
1920
using System.Threading.Tasks;
21+
using System.Xml.Linq;
2022

2123
namespace ExchangeSharp
2224
{
@@ -25,10 +27,10 @@ namespace ExchangeSharp
2527
/// If you are using legacy API keys from previous Coinbase versions they must be upgraded to Advanced Trade on the Coinbase site.
2628
/// These keys must be set before using the Coinbase API (sorry).
2729
/// </summary>
28-
public sealed partial class ExchangeCoinbaseAPI : ExchangeAPI
30+
public partial class ExchangeCoinbaseAPI : ExchangeAPI
2931
{
3032
public override string BaseUrl { get; set; } = "https://api.coinbase.com/api/v3/brokerage";
31-
private readonly string BaseUrlV2 = "https://api.coinbase.com/v2"; // For Wallet Support
33+
protected readonly string BaseUrlV2 = "https://api.coinbase.com/v2"; // For Wallet Support
3234
public override string BaseUrlWebSocket { get; set; } = "wss://advanced-trade-ws.coinbase.com";
3335

3436
private enum PaginationType { None, V2, V3}
@@ -37,7 +39,7 @@ private enum PaginationType { None, V2, V3}
3739

3840
private Dictionary<string, string> Accounts = null; // Cached Account IDs
3941

40-
private ExchangeCoinbaseAPI()
42+
protected ExchangeCoinbaseAPI()
4143
{
4244
MarketSymbolIsUppercase = true;
4345
MarketSymbolIsReversed = false;
@@ -85,19 +87,11 @@ protected override bool CanMakeAuthenticatedRequest(IReadOnlyDictionary<string,
8587
protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary<string, object> payload)
8688
{
8789
if (CanMakeAuthenticatedRequest(payload))
88-
{
89-
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant(); // If you're skittish about the local clock, you may retrieve the timestamp from the Coinbase Site
90-
string body = CryptoUtility.GetJsonForPayload(payload);
91-
92-
// V2 wants PathAndQuery, V3 wants LocalPath for the sig (I guess they wanted to shave a nano-second or two - silly)
93-
string path = request.RequestUri.AbsoluteUri.StartsWith(BaseUrlV2) ? request.RequestUri.PathAndQuery : request.RequestUri.LocalPath;
94-
string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString());
95-
96-
request.AddHeader("CB-ACCESS-KEY", PublicApiKey.ToUnsecureString());
97-
request.AddHeader("CB-ACCESS-SIGN", signature);
98-
request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp);
99-
if (request.Method == "POST") await CryptoUtility.WriteToRequestAsync(request, body);
100-
}
90+
{
91+
string endpoint = $"{request.RequestUri.Host}{request.RequestUri.AbsolutePath}";
92+
string token = GenerateToken(PublicApiKey.ToUnsecureString(), PrivateApiKey.ToUnsecureString(), $"{request.Method} {endpoint}");
93+
request.AddHeader("Authorization", $"Bearer {token}");
94+
}
10195
}
10296

10397
/// <summary>

src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI_Const.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace ExchangeSharp
22
{
3-
public sealed partial class ExchangeCoinbaseAPI
3+
public partial class ExchangeCoinbaseAPI
44
{
55
private const string ADVFILL = "advanced_trade_fill";
66
private const string AMOUNT = "amount";
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System;
2+
using System.IdentityModel.Tokens.Jwt;
3+
using System.Net.Http;
4+
using System.Security.Cryptography;
5+
using Microsoft.IdentityModel.Tokens;
6+
using Org.BouncyCastle.Crypto;
7+
using Org.BouncyCastle.Crypto.Parameters;
8+
using Org.BouncyCastle.OpenSsl;
9+
using Org.BouncyCastle.Security;
10+
using System.IO;
11+
12+
namespace ExchangeSharp
13+
{
14+
public partial class ExchangeCoinbaseAPI
15+
{ // Currently using .NET 4.7.2 version of code from https://docs.cdp.coinbase.com/advanced-trade/docs/rest-api-auth
16+
// since we currently target netstandard2.0. If we upgrade in the future, we can change to the simpler .NET core code
17+
static string GenerateToken(string name, string privateKeyPem, string uri)
18+
{
19+
// Load EC private key using BouncyCastle
20+
var ecPrivateKey = LoadEcPrivateKeyFromPem(privateKeyPem);
21+
22+
// Create security key from the manually created ECDsa
23+
var ecdsa = GetECDsaFromPrivateKey(ecPrivateKey);
24+
var securityKey = new ECDsaSecurityKey(ecdsa);
25+
26+
// Signing credentials
27+
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);
28+
29+
var now = DateTimeOffset.UtcNow;
30+
31+
// Header and payload
32+
var header = new JwtHeader(credentials);
33+
header["kid"] = name;
34+
header["nonce"] = GenerateNonce(); // Generate dynamic nonce
35+
36+
var payload = new JwtPayload
37+
{
38+
{ "iss", "coinbase-cloud" },
39+
{ "sub", name },
40+
{ "nbf", now.ToUnixTimeSeconds() },
41+
{ "exp", now.AddMinutes(2).ToUnixTimeSeconds() },
42+
{ "uri", uri }
43+
};
44+
45+
var token = new JwtSecurityToken(header, payload);
46+
47+
var tokenHandler = new JwtSecurityTokenHandler();
48+
return tokenHandler.WriteToken(token);
49+
}
50+
51+
// Method to generate a dynamic nonce
52+
static string GenerateNonce(int length = 64)
53+
{
54+
byte[] nonceBytes = new byte[length / 2]; // Allocate enough space for the desired length (in hex characters)
55+
using (var rng = RandomNumberGenerator.Create())
56+
{
57+
rng.GetBytes(nonceBytes);
58+
}
59+
return BitConverter.ToString(nonceBytes).Replace("-", "").ToLower(); // Convert byte array to hex string
60+
}
61+
62+
// Method to load EC private key from PEM using BouncyCastle
63+
static ECPrivateKeyParameters LoadEcPrivateKeyFromPem(string privateKeyPem)
64+
{
65+
using (var stringReader = new StringReader(privateKeyPem))
66+
{
67+
var pemReader = new PemReader(stringReader);
68+
var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
69+
if (keyPair == null)
70+
throw new InvalidOperationException("Failed to load EC private key from PEM");
71+
72+
return (ECPrivateKeyParameters)keyPair.Private;
73+
}
74+
}
75+
76+
// Method to convert ECPrivateKeyParameters to ECDsa
77+
static ECDsa GetECDsaFromPrivateKey(ECPrivateKeyParameters privateKey)
78+
{
79+
var q = privateKey.Parameters.G.Multiply(privateKey.D).Normalize();
80+
var qx = q.AffineXCoord.GetEncoded();
81+
var qy = q.AffineYCoord.GetEncoded();
82+
83+
var ecdsaParams = new ECParameters
84+
{
85+
Curve = ECCurve.NamedCurves.nistP256, // Adjust if you're using a different curve
86+
Q =
87+
{
88+
X = qx,
89+
Y = qy
90+
},
91+
D = privateKey.D.ToByteArrayUnsigned()
92+
};
93+
94+
return ECDsa.Create(ecdsaParams);
95+
}
96+
}
97+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
6+
namespace ExchangeSharp.Coinbase
7+
{
8+
/// <summary>
9+
/// partial implementation for Coinbase Exchange, which is for businesses (rather than Advanced which is for individuals). Since there may not be many users of Coinbase Exchange, will not expose this for now to avoid confusion
10+
/// </summary>
11+
public sealed partial class ExchangeCoinbaseExchangeAPI : ExchangeCoinbaseAPI
12+
{
13+
protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary<string, object> payload)
14+
{ // Coinbase Exchange uses the old signing method rather than JWT
15+
if (CanMakeAuthenticatedRequest(payload))
16+
{
17+
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant(); // If you're skittish about the local clock, you may retrieve the timestamp from the Coinbase Site
18+
string body = CryptoUtility.GetJsonForPayload(payload);
19+
20+
// V2 wants PathAndQuery, V3 wants LocalPath for the sig (I guess they wanted to shave a nano-second or two - silly)
21+
string path = request.RequestUri.AbsoluteUri.StartsWith(BaseUrlV2) ? request.RequestUri.PathAndQuery : request.RequestUri.LocalPath;
22+
string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString());
23+
24+
request.AddHeader("CB-ACCESS-KEY", PublicApiKey.ToUnsecureString());
25+
request.AddHeader("CB-ACCESS-SIGN", signature);
26+
request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp);
27+
if (request.Method == "POST") await CryptoUtility.WriteToRequestAsync(request, body);
28+
}
29+
}
30+
}
31+
}

src/ExchangeSharp/ExchangeSharp.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
</ItemGroup>
3434

3535
<ItemGroup>
36+
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
3637
<PackageReference Include="Microsoft.AspNet.SignalR.Client" Version="2.4.3" />
3738
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
3839
<PrivateAssets>all</PrivateAssets>
@@ -42,6 +43,7 @@
4243
<PackageReference Include="NLog" Version="5.3.4" />
4344
<PackageReference Include="SocketIOClient" Version="3.1.2" />
4445
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.0" />
46+
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
4547
</ItemGroup>
4648

4749
<ItemGroup>

0 commit comments

Comments
 (0)