Skip to content

Commit

Permalink
implement new Coinbase JWT Bearer token for Authentication header (#857)
Browse files Browse the repository at this point in the history
  • Loading branch information
vslee authored Nov 25, 2024
1 parent 09a30c9 commit b44075e
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 17 deletions.
26 changes: 10 additions & 16 deletions src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ The above copyright notice and this permission notice shall be included in all c
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Xml.Linq;

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

private enum PaginationType { None, V2, V3}
Expand All @@ -37,7 +39,7 @@ private enum PaginationType { None, V2, V3}

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

private ExchangeCoinbaseAPI()
protected ExchangeCoinbaseAPI()
{
MarketSymbolIsUppercase = true;
MarketSymbolIsReversed = false;
Expand Down Expand Up @@ -85,19 +87,11 @@ protected override bool CanMakeAuthenticatedRequest(IReadOnlyDictionary<string,
protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary<string, object> payload)
{
if (CanMakeAuthenticatedRequest(payload))
{
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant(); // If you're skittish about the local clock, you may retrieve the timestamp from the Coinbase Site
string body = CryptoUtility.GetJsonForPayload(payload);

// V2 wants PathAndQuery, V3 wants LocalPath for the sig (I guess they wanted to shave a nano-second or two - silly)
string path = request.RequestUri.AbsoluteUri.StartsWith(BaseUrlV2) ? request.RequestUri.PathAndQuery : request.RequestUri.LocalPath;
string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString());

request.AddHeader("CB-ACCESS-KEY", PublicApiKey.ToUnsecureString());
request.AddHeader("CB-ACCESS-SIGN", signature);
request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp);
if (request.Method == "POST") await CryptoUtility.WriteToRequestAsync(request, body);
}
{
string endpoint = $"{request.RequestUri.Host}{request.RequestUri.AbsolutePath}";
string token = GenerateToken(PublicApiKey.ToUnsecureString(), PrivateApiKey.ToUnsecureString(), $"{request.Method} {endpoint}");
request.AddHeader("Authorization", $"Bearer {token}");
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace ExchangeSharp
{
public sealed partial class ExchangeCoinbaseAPI
public partial class ExchangeCoinbaseAPI
{
private const string ADVFILL = "advanced_trade_fill";
private const string AMOUNT = "amount";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System.IO;

namespace ExchangeSharp
{
public partial class ExchangeCoinbaseAPI
{ // Currently using .NET 4.7.2 version of code from https://docs.cdp.coinbase.com/advanced-trade/docs/rest-api-auth
// since we currently target netstandard2.0. If we upgrade in the future, we can change to the simpler .NET core code
static string GenerateToken(string name, string privateKeyPem, string uri)
{
// Load EC private key using BouncyCastle
var ecPrivateKey = LoadEcPrivateKeyFromPem(privateKeyPem);

// Create security key from the manually created ECDsa
var ecdsa = GetECDsaFromPrivateKey(ecPrivateKey);
var securityKey = new ECDsaSecurityKey(ecdsa);

// Signing credentials
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256);

var now = DateTimeOffset.UtcNow;

// Header and payload
var header = new JwtHeader(credentials);
header["kid"] = name;
header["nonce"] = GenerateNonce(); // Generate dynamic nonce

var payload = new JwtPayload
{
{ "iss", "coinbase-cloud" },
{ "sub", name },
{ "nbf", now.ToUnixTimeSeconds() },
{ "exp", now.AddMinutes(2).ToUnixTimeSeconds() },
{ "uri", uri }
};

var token = new JwtSecurityToken(header, payload);

var tokenHandler = new JwtSecurityTokenHandler();
return tokenHandler.WriteToken(token);
}

// Method to generate a dynamic nonce
static string GenerateNonce(int length = 64)
{
byte[] nonceBytes = new byte[length / 2]; // Allocate enough space for the desired length (in hex characters)
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(nonceBytes);
}
return BitConverter.ToString(nonceBytes).Replace("-", "").ToLower(); // Convert byte array to hex string
}

// Method to load EC private key from PEM using BouncyCastle
static ECPrivateKeyParameters LoadEcPrivateKeyFromPem(string privateKeyPem)
{
using (var stringReader = new StringReader(privateKeyPem))
{
var pemReader = new PemReader(stringReader);
var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
if (keyPair == null)
throw new InvalidOperationException("Failed to load EC private key from PEM");

return (ECPrivateKeyParameters)keyPair.Private;
}
}

// Method to convert ECPrivateKeyParameters to ECDsa
static ECDsa GetECDsaFromPrivateKey(ECPrivateKeyParameters privateKey)
{
var q = privateKey.Parameters.G.Multiply(privateKey.D).Normalize();
var qx = q.AffineXCoord.GetEncoded();
var qy = q.AffineYCoord.GetEncoded();

var ecdsaParams = new ECParameters
{
Curve = ECCurve.NamedCurves.nistP256, // Adjust if you're using a different curve
Q =
{
X = qx,
Y = qy
},
D = privateKey.D.ToByteArrayUnsigned()
};

return ECDsa.Create(ecdsaParams);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace ExchangeSharp.Coinbase
{
/// <summary>
/// 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
/// </summary>
public sealed partial class ExchangeCoinbaseExchangeAPI : ExchangeCoinbaseAPI
{
protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary<string, object> payload)
{ // Coinbase Exchange uses the old signing method rather than JWT
if (CanMakeAuthenticatedRequest(payload))
{
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant(); // If you're skittish about the local clock, you may retrieve the timestamp from the Coinbase Site
string body = CryptoUtility.GetJsonForPayload(payload);

// V2 wants PathAndQuery, V3 wants LocalPath for the sig (I guess they wanted to shave a nano-second or two - silly)
string path = request.RequestUri.AbsoluteUri.StartsWith(BaseUrlV2) ? request.RequestUri.PathAndQuery : request.RequestUri.LocalPath;
string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString());

request.AddHeader("CB-ACCESS-KEY", PublicApiKey.ToUnsecureString());
request.AddHeader("CB-ACCESS-SIGN", signature);
request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp);
if (request.Method == "POST") await CryptoUtility.WriteToRequestAsync(request, body);
}
}
}
}
2 changes: 2 additions & 0 deletions src/ExchangeSharp/ExchangeSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
<PackageReference Include="Microsoft.AspNet.SignalR.Client" Version="2.4.3" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<PrivateAssets>all</PrivateAssets>
Expand All @@ -42,6 +43,7 @@
<PackageReference Include="NLog" Version="5.3.4" />
<PackageReference Include="SocketIOClient" Version="3.1.2" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit b44075e

Please sign in to comment.