Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coinbase: implement new JWT Bearer token for Authentication header #857

Merged
merged 1 commit into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading