Skip to content

Commit

Permalink
JWT move and other fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
cranktakular committed Nov 27, 2024
1 parent a4252ec commit ff6ce5f
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 183 deletions.
101 changes: 71 additions & 30 deletions exchanges/coinbasepro/coinbasepro.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package coinbasepro
import (
"bytes"
"context"
"encoding/hex"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"net/http"
Expand All @@ -14,8 +15,8 @@ import (
"time"

"github.com/gofrs/uuid"
"github.com/golang-jwt/jwt"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
Expand Down Expand Up @@ -155,6 +156,7 @@ var (
errUnrecognisedAssetType = errors.New("unrecognised asset type")
errUnrecognisedStrategyType = errors.New("unrecognised strategy type")
errIntervalNotSupported = errors.New("interval not supported")
errEndpointPathInvalid = errors.New("endpoint path invalid, should start with https://")

allowedGranularities = []string{granOneMin, granFiveMin, granFifteenMin, granThirtyMin, granOneHour, granTwoHour, granSixHour, granOneDay}
closedStatuses = []string{"FILLED", "CANCELLED", "EXPIRED", "FAILED"}
Expand Down Expand Up @@ -428,6 +430,7 @@ func (c *CoinbasePro) GetAllOrders(ctx context.Context, productID, userNativeCur
if contractExpiryType != "" {
params.Values.Set("contract_expiry_type", contractExpiryType)
}
// This functionality has been deprecated, and only works for legacy API keys
if retailPortfolioID != "" {
params.Values.Set("retail_portfolio_id", retailPortfolioID)
}
Expand Down Expand Up @@ -1396,19 +1399,14 @@ func (c *CoinbasePro) SendHTTPRequest(ctx context.Context, ep exchange.URL, path

// SendAuthenticatedHTTPRequest sends an authenticated HTTP request
func (c *CoinbasePro) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, method, path string, queryParams url.Values, bodyParams map[string]any, isVersion3 bool, result any, returnHead *http.Header) (err error) {
creds, err := c.GetCredentials(ctx)
if err != nil {
return err
}
endpoint, err := c.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
queryString := common.EncodeURLValues("", queryParams)
// Version 2 wants query params in the path during signing
if !isVersion3 {
path += queryString
if len(endpoint) < 8 {
return errEndpointPathInvalid
}
queryString := common.EncodeURLValues("", queryParams)
interim := json.RawMessage{}
newRequest := func() (*request.Item, error) {
payload := []byte("")
Expand All @@ -1418,31 +1416,15 @@ func (c *CoinbasePro) SendAuthenticatedHTTPRequest(ctx context.Context, ep excha
return nil, err
}
}
n := strconv.FormatInt(time.Now().Unix(), 10)
message := n + method + path + string(payload)
var hmac []byte
hmac, err = crypto.GetHMAC(crypto.HashSHA256,
[]byte(message),
[]byte(creds.Secret))
jwt, _, err := c.GetJWT(ctx, method+" "+endpoint[8:]+path)

Check failure on line 1419 in exchanges/coinbasepro/coinbasepro.go

View workflow job for this annotation

GitHub Actions / lint

shadow: declaration of "err" shadows declaration at line 1401 (govet)
if err != nil {
return nil, err
}
// TODO: Implement JWT authentication once it's supported by all endpoints we care about
// jwt, err := c.GetJWT(ctx, method+" "+path)
// if err != nil {
// return nil, err
// }
headers := make(map[string]string)
headers["CB-ACCESS-KEY"] = creds.Key
headers["CB-ACCESS-SIGN"] = hex.EncodeToString(hmac)
headers["CB-ACCESS-TIMESTAMP"] = n
headers["Content-Type"] = "application/json"
headers["CB-VERSION"] = "2024-09-24"
// headers["Authorization"] = "Bearer " + jwt
// Version 3 only wants query params in the path when the request is sent
if isVersion3 {
path += queryString
}
headers["CB-VERSION"] = "2024-11-27"
headers["Authorization"] = "Bearer " + jwt
path += queryString
return &request.Item{
Method: method,
Path: endpoint + path,
Expand Down Expand Up @@ -1505,6 +1487,65 @@ func (c *CoinbasePro) SendAuthenticatedHTTPRequest(ctx context.Context, ep excha
return json.Unmarshal(interim, result)
}

// GetJWT generates a new JWT
func (c *CoinbasePro) GetJWT(ctx context.Context, uri string) (string, time.Time, error) {
creds, err := c.GetCredentials(ctx)
if err != nil {
return "", time.Time{}, err
}
block, _ := pem.Decode([]byte(creds.Secret))
if block == nil {
return "", time.Time{}, errCantDecodePrivKey
}
key, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return "", time.Time{}, err
}
nonce, err := common.GenerateRandomString(16, "1234567890ABCDEF")
if err != nil {
return "", time.Time{}, err
}
regTime := time.Now()
mapClaims := jwt.MapClaims{
"iss": "cdp",
"nbf": regTime.Unix(),
"exp": regTime.Add(time.Minute * 2).Unix(),
"sub": creds.Key,
}
if uri != "" {
mapClaims["uri"] = uri
}
tok := jwt.NewWithClaims(jwt.SigningMethodES256, mapClaims)
tok.Header["kid"] = creds.Key
tok.Header["nonce"] = nonce
sign, err := tok.SignedString(key)
return sign, regTime, err
// The code below mostly works, but seems to lead to bad results on the signature step. Deferring until later
// head := map[string]any{"kid": creds.Key, "typ": "JWT", "alg": "ES256", "nonce": nonce}
// headJSON, err := json.Marshal(head)
// if err != nil {
// return "", time.Time{}, err
// }
// headEncode := base64URLEncode(headJSON)
// regTime := time.Now()
// body := map[string]any{"iss": "cdp", "nbf": regTime.Unix(), "exp": regTime.Add(time.Minute * 2).Unix(), "sub": creds.Key /*, "aud": "retail_rest_api_proxy"*/}
// if uri != "" {
// body["uri"] = uri
// }
// bodyJSON, err := json.Marshal(body)
// if err != nil {
// return "", time.Time{}, err
// }
// bodyEncode := base64URLEncode(bodyJSON)
// hash := sha256.Sum256([]byte(headEncode + "." + bodyEncode))
// sig, err := ecdsa.SignASN1(rand.Reader, key, hash[:])
// if err != nil {
// return "", time.Time{}, err
// }
// sigEncode := base64URLEncode(sig)
// return headEncode + "." + bodyEncode + "." + sigEncode, regTime, nil
}

// GetFee returns an estimate of fee based on type of transaction
func (c *CoinbasePro) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
if feeBuilder == nil {
Expand Down
22 changes: 11 additions & 11 deletions exchanges/coinbasepro/coinbasepro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ func TestGetAllOrders(t *testing.T) {
status = make([]string, 0)
assets = make([]string, 1)
assets[0] = testCrypto.String()
_, err = c.GetAllOrders(context.Background(), "", testFiat.String(), "LIMIT", "SELL", "", "SPOT", "RETAIL_ADVANCED", "UNKNOWN_CONTRACT_EXPIRY_TYPE", "2", status, assets, 10, time.Time{}, time.Time{})
_, err = c.GetAllOrders(context.Background(), "", testFiat.String(), "LIMIT", "SELL", "", "SPOT", "RETAIL_ADVANCED", "UNKNOWN_CONTRACT_EXPIRY_TYPE", "", status, assets, 10, time.Time{}, time.Time{})
assert.NoError(t, err)
}

Expand Down Expand Up @@ -435,6 +435,7 @@ func TestDeletePortfolio(t *testing.T) {
assert.ErrorIs(t, err, errPortfolioIDEmpty)
pID := portfolioIDFromName(t, "GCT Test Portfolio To-Delete")
err = c.DeletePortfolio(context.Background(), pID)
// The new JWT-based keys don't have permissions to delete portfolios they aren't assigned to, causing this to fail
assert.NoError(t, err)
}

Expand All @@ -446,6 +447,7 @@ func TestEditPortfolio(t *testing.T) {
assert.ErrorIs(t, err, errNameEmpty)
pID := portfolioIDFromName(t, "GCT Test Portfolio To-Edit")
_, err = c.EditPortfolio(context.Background(), pID, "GCT Test Portfolio Edited")
// The new JWT-based keys don't have permissions to edit portfolios they aren't assigned to, causing this to fail
if err != nil && err.Error() != errPortfolioNameDuplicate {
t.Error(err)
}
Expand Down Expand Up @@ -586,6 +588,7 @@ func TestGetPaymentMethodByID(t *testing.T) {
func TestGetCurrentUser(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, c)
// This intermittently fails with the message "Unauthorized", for no clear reason
testGetNoArgs(t, c.GetCurrentUser)
}

Expand All @@ -595,7 +598,7 @@ func TestGetAllWallets(t *testing.T) {
pagIn := PaginationInp{Limit: 2}
resp, err := c.GetAllWallets(context.Background(), pagIn)
assert.NoError(t, err)
assert.NotEmpty(t, resp, errExpectedNonEmpty)
require.NotEmpty(t, resp, errExpectedNonEmpty)
if resp.Pagination.NextStartingAfter == "" {
t.Skip(skipInsufficientWallets)
}
Expand Down Expand Up @@ -963,11 +966,7 @@ func TestSendHTTPRequest(t *testing.T) {

func TestSendAuthenticatedHTTPRequest(t *testing.T) {
t.Parallel()
fc := &CoinbasePro{}
err := fc.SendAuthenticatedHTTPRequest(context.Background(), exchange.EdgeCase3, "", "", nil, nil, false, nil, nil)
assert.ErrorIs(t, err, exchange.ErrCredentialsAreEmpty)
sharedtestvalues.SkipTestIfCredentialsUnset(t, c)
err = c.SendAuthenticatedHTTPRequest(context.Background(), exchange.EdgeCase3, "", "", nil, nil, false, nil, nil)
err := c.SendAuthenticatedHTTPRequest(context.Background(), exchange.EdgeCase3, "", "", nil, nil, false, nil, nil)
assert.ErrorIs(t, err, exchange.ErrEndpointPathNotFound)
ch := make(chan struct{})
body := map[string]any{"Unmarshalable": ch}
Expand Down Expand Up @@ -1518,9 +1517,10 @@ func TestWsAuth(t *testing.T) {
go c.wsReadData()
err = c.Subscribe(subscription.List{
{
Channel: "myAccount",
Asset: asset.All,
Pairs: p,
Channel: "myAccount",
Asset: asset.All,
Pairs: p,
Authenticated: true,
},
})
assert.NoError(t, err)
Expand Down Expand Up @@ -1705,7 +1705,7 @@ func TestCheckSubscriptions(t *testing.T) {
func TestGetJWT(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, c)
_, err := c.GetJWT(context.Background(), "")
_, _, err := c.GetJWT(context.Background(), "")
assert.NoError(t, err)
}

Expand Down
Loading

0 comments on commit ff6ce5f

Please sign in to comment.