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

Feature: new Authentication process #23

Merged
merged 17 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
4 changes: 2 additions & 2 deletions .github/workflows/gh-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ jobs:
build:
runs-on: ubuntu-20.04
env:
QC_COINBASE_API_SECRET: ${{ secrets.QC_COINBASE_API_SECRET }}
QC_COINBASE_API_KEY: ${{ secrets.QC_COINBASE_API_KEY }}
QC_COINBASE_API_NAME: ${{ secrets.QC_COINBASE_API_NAME }}
QC_COINBASE_API_PRIVATE_KEY: ${{ secrets.QC_COINBASE_API_PRIVATE_KEY }}
QC_COINBASE_URL: ${{ secrets.QC_COINBASE_URL }}
QC_COINBASE_REST_API: ${{ secrets.QC_COINBASE_REST_API }}
QC_JOB_USER_ID: ${{ secrets.JOB_USER_ID }}
Expand Down
136 changes: 115 additions & 21 deletions QuantConnect.CoinbaseBrokerage.Tests/CoinbaseApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
using Newtonsoft.Json.Linq;
using QuantConnect.Configuration;
using System.Collections.Generic;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using QuantConnect.Brokerages.Coinbase.Api;
using QuantConnect.Brokerages.Coinbase.Models.Enums;
using QuantConnect.Brokerages.Coinbase.Models.WebSocket;
Expand All @@ -35,30 +38,37 @@ public class CoinbaseApiTests
[SetUp]
public void Setup()
{
var apiKey = Config.Get("coinbase-api-key");
var apiKeySecret = Config.Get("coinbase-api-secret");
var name = Config.Get("coinbase-api-name");
var priavteKey = Config.Get("coinbase-api-private-key");

CoinbaseApi = CreateCoinbaseApi(apiKey, apiKeySecret);
CoinbaseApi = CreateCoinbaseApi(name, priavteKey);
}

[TestCase("", "")]
[TestCase("1", "2")]
public void InvalidAuthenticationCredentialsShouldThrowException(string apiKey, string apiKeySecret)
[TestCase("", "", typeof(ArgumentOutOfRangeException))]
[TestCase("organizations/2c7dhs-a3a3-4acf-aa0c-f68584f34c37/apiKeys/41090ffa-asd2-4040-815f-afaf63747e35", "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIPcJGfXYEdLQi0iFj1xvGfPwuRNoeddwuKS4xL2NrlGWpoAoGCCqGSM49\nAwEHoUQDQgAEclN+asd/EhJ3UjOWkHmP/iqGBv5NkNJ75bUq\nVgxS4aU3/djHiIuSf27QasdOFIDGJLmOn7YiQ==\n-----END EC PRIVATE KEY-----\n", typeof(System.Security.Cryptography.CryptographicException))]
public void InvalidAuthenticationCredentialsShouldThrowException(string name, string privateKey, Type expectedException)
{
var coinbaseApi = CreateCoinbaseApi(apiKey, apiKeySecret);
try
{
var coinbaseApi = CreateCoinbaseApi(name, privateKey);

// call random endpoint with incorrect credential
Assert.Throws<Exception>(() => coinbaseApi.GetAccounts());
// call random endpoint with incorrect credential
Assert.Throws(expectedException, () => coinbaseApi.GetAccounts());
}
catch (Exception ex)
{
Assert.IsInstanceOf(expectedException, ex);
}
}

[Test]
public void GetListAccounts()
[Test]
public void GetListAccounts()
{
var accounts = CoinbaseApi.GetAccounts();

Assert.Greater(accounts.Count(), 0);

foreach(var account in accounts)
foreach (var account in accounts)
{
Assert.IsTrue(account.Active);
Assert.IsNotEmpty(account.Name);
Expand Down Expand Up @@ -161,22 +171,22 @@ public void ParseWebSocketLevel2DataResponse()
Assert.GreaterOrEqual(tick.NewQuantity, 0);
Assert.GreaterOrEqual(tick.PriceLevel, 0);
Assert.IsInstanceOf<CoinbaseLevel2UpdateSide>(tick.Side);
}
}
}

[TestCase("/api/v3/brokerage/orders", null, "Unauthorized")]
[TestCase("/api/v3/brokerage/orders", "", "Unauthorized")]
[TestCase("/api/v3/brokerage/orders", null, "Bad Request")]
[TestCase("/api/v3/brokerage/orders", "", "Bad Request")]
[TestCase("/api/v3/brokerage/orders", "{null}", "Bad Request")]
[TestCase("/api/v3/brokerage/orders", "[]", "Unauthorized")]
[TestCase("/api/v3/brokerage/orders", "[]", "Bad Request")]
public void ValidateCoinbaseRestRequestWithWrongBodyParameter(string uriPath, object bodyData, string message)
{
var apiKey = Config.Get("coinbase-api-key");
var apiKeySecret = Config.Get("coinbase-api-secret");
var name = Config.Get("coinbase-api-name");
var privateKey = Config.Get("coinbase-api-private-key");
var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com");

var request = new RestRequest($"{uriPath}", Method.POST);

var _apiClient = new CoinbaseApiClient(apiKey, apiKeySecret, restApiUrl, 30);
var _apiClient = new CoinbaseApiClient(name, privateKey, restApiUrl, 30);

request.AddJsonBody(bodyData);

Expand All @@ -199,11 +209,95 @@ public void CancelOrderWithWrongOrderId(string orderId, string errorMessage)
Assert.AreEqual(errorMessage, response.FailureReason);
}

private CoinbaseApi CreateCoinbaseApi(string apiKey, string apiKeySecret)
[Test]
public void ValidateGenerateWebSocketJWTToken()
{
var name = Config.Get("coinbase-api-name");
var privateKey = Config.Get("coinbase-api-private-key");
var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com");

var _apiClient = new CoinbaseApiClient(name, privateKey, restApiUrl, 30);

var generateWebSocketJWTToken = _apiClient.GenerateWebSocketToken();

var parsedPrivateKey = _apiClient.ParseKey(privateKey);

Assert.IsTrue(IsTokenValid(generateWebSocketJWTToken, name, parsedPrivateKey));
}

/// <summary>
/// Validates a JWT token using ECDsa key with the specified token ID and secret.
/// </summary>
/// <param name="token">The JWT token to be validated.</param>
/// <param name="tokenId">The unique identifier for the ECDsa security key.</param>
/// <param name="parsedPrivateKey">The ECDsa private key in Base64 format used to validate the token's signature.</param>
/// <returns>
/// <c>true</c> if the token is successfully validated; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// This method is useful for verifying the authenticity of JWT tokens using ECDsa keys.
/// It ensures that the token's signature matches the expected signature derived from the provided secret.
/// </remarks>
private bool IsTokenValid(string token, string tokenId, string parsedPrivateKey)
{
if (token == null)
return false;

var key = ECDsa.Create();
key?.ImportECPrivateKey(Convert.FromBase64String(parsedPrivateKey), out _);

var securityKey = new ECDsaSecurityKey(key) { KeyId = tokenId };

try
{
var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = securityKey,
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.FromSeconds(100),
ValidateLifetime = true,
LifetimeValidator = CustomLifetimeValidator,
}, out var validatedToken);

return true;
}
catch
{
return false;
}
}

/// <summary>
/// Custom validator for checking the token's lifetime.
/// </summary>
/// <param name="notBefore">The 'Not Before' date/time from the token's claims.</param>
/// <param name="expires">The expiration date/time from the token's claims.</param>
/// <param name="tokenToValidate">The security token being validated.</param>
/// <param name="param">The token validation parameters.</param>
/// <returns>
/// <c>true</c> if the token is valid based on its expiration time; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// This custom lifetime validator ensures that the JWT token has not expired. It compares the
/// token's expiration time against the current UTC time to determine its validity.
/// </remarks>
private static bool CustomLifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken tokenToValidate, TokenValidationParameters @param)
{
if (expires != null)
{
return expires > DateTime.UtcNow;
}
return false;
}

private CoinbaseApi CreateCoinbaseApi(string name, string privateKey)
{
var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com");

return new CoinbaseApi(new SymbolPropertiesDatabaseSymbolMapper(Market.Coinbase), null, apiKey, apiKeySecret, restApiUrl);
return new CoinbaseApi(new SymbolPropertiesDatabaseSymbolMapper(Market.Coinbase), null, name, privateKey, restApiUrl);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,11 @@ private static CoinbaseBrokerage GetBrokerage()
{
var wssUrl = Config.Get("coinbase-url", "wss://advanced-trade-ws.coinbase.com");
var restApiUrl = Config.Get("coinbase-rest-api", "https://api.coinbase.com");
var apiKey = Config.Get("coinbase-api-key");
var apiSecret = Config.Get("coinbase-api-secret");
var name = Config.Get("coinbase-api-name");
var privateKey = Config.Get("coinbase-api-private-key");
var algorithm = new QCAlgorithm();
var aggregator = new AggregationManager();

return new CoinbaseBrokerage(wssUrl, apiKey, apiSecret, restApiUrl, algorithm, aggregator, null);
return new CoinbaseBrokerage(wssUrl, name, privateKey, restApiUrl, algorithm, null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
using NUnit.Framework;
using System.Threading;
using QuantConnect.Data;
using QuantConnect.Tests;
using QuantConnect.Logging;
using Microsoft.CodeAnalysis;
using QuantConnect.Data.Market;
using System.Collections.Generic;
using QuantConnect.Tests;
using System.Collections.Concurrent;

namespace QuantConnect.Brokerages.Coinbase.Tests
{
Expand All @@ -46,7 +47,7 @@ private static IEnumerable<TestCaseData> TestParameters
yield return new TestCaseData(BTCUSDC, Resolution.Second);
yield return new TestCaseData(BTCUSDC, Resolution.Minute);
yield return new TestCaseData(Symbol.Create("ETHUSD", SecurityType.Crypto, Market.Coinbase), Resolution.Minute);
yield return new TestCaseData(Symbol.Create("GRTUSD", SecurityType.Crypto, Market.Coinbase), Resolution.Second);
yield return new TestCaseData(Symbol.Create("SOLUSD", SecurityType.Crypto, Market.Coinbase), Resolution.Second);
}
}

Expand Down Expand Up @@ -193,7 +194,9 @@ public void SubscribeOnMultipleSymbols(List<SubscriptionDataConfig> liquidSymbol
{
var cancelationToken = new CancellationTokenSource();
var startTime = DateTime.UtcNow;
var obj = new object();
var tickResetEvent = new ManualResetEvent(false);
var minimumAvailableReturnResponseData = liquidSymbolsSubscriptionConfigs.Count / 2;

_brokerage.Message += (_, brokerageMessageEvent) =>
{
Expand All @@ -202,7 +205,7 @@ public void SubscribeOnMultipleSymbols(List<SubscriptionDataConfig> liquidSymbol
cancelationToken.Cancel();
};

var symbolTicks = new Dictionary<Symbol, bool>();
var symbolTicks = new ConcurrentDictionary<Symbol, bool>();
foreach (var config in liquidSymbolsSubscriptionConfigs)
{
ProcessFeed(_brokerage.Subscribe(config, (s, e) => { }),
Expand All @@ -218,14 +221,18 @@ public void SubscribeOnMultipleSymbols(List<SubscriptionDataConfig> liquidSymbol
Assert.IsTrue(tick.Price > 0, "Price was not greater then zero");
Assert.IsTrue(tick.Value > 0, "Value was not greater then zero");

if (!symbolTicks.TryGetValue(tick.Symbol, out var symbol))
lock (obj)
{
symbolTicks[tick.Symbol] = true;
}
if (!symbolTicks.TryGetValue(tick.Symbol, out var symbol))
{
_brokerage.Unsubscribe(config);
symbolTicks[tick.Symbol] = true;
}

if (symbolTicks.Count == liquidSymbolsSubscriptionConfigs.Count / 2)
{
tickResetEvent.Set();
if (symbolTicks.Count == minimumAvailableReturnResponseData)
{
tickResetEvent.Set();
}
}
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
using QuantConnect.Data.Market;
using System.Collections.Generic;
using QuantConnect.Configuration;
using QuantConnect.Lean.Engine.DataFeeds;

namespace QuantConnect.Brokerages.Coinbase.Tests
{
Expand All @@ -36,11 +35,10 @@ public void GetsHistory(Symbol symbol, Resolution resolution, TickType tickType,
{
var brokerage = new CoinbaseBrokerage(
Config.Get("coinbase-url", "wss://advanced-trade-ws.coinbase.com"),
Config.Get("coinbase-api-key"),
Config.Get("coinbase-api-secret"),
Config.Get("coinbase-api-name"),
Config.Get("coinbase-api-private-key"),
Config.Get("coinbase-rest-api", "https://api.coinbase.com"),
null,
new AggregationManager(),
null);

var now = DateTime.UtcNow;
Expand Down
Loading