Skip to content

Instantly share code, notes, and snippets.

@IamNator
Last active January 18, 2024 16:45
Show Gist options
  • Save IamNator/80c32850240f97497464a90a67432212 to your computer and use it in GitHub Desktop.
Save IamNator/80c32850240f97497464a90a67432212 to your computer and use it in GitHub Desktop.
fireblock-auth
package sdk
import (
"bytes"
crand "crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"strings"
"time"
"github.com/gojek/heimdall/v7/hystrix"
"github.com/golang-jwt/jwt"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
)
type FbKeyMgmt struct {
privateKey *rsa.PrivateKey
apiKey string
rnd *rand.Rand
}
func NewInstanceKeyMgmt(pk *rsa.PrivateKey, apiKey string) *FbKeyMgmt {
var s secrets
k := new(FbKeyMgmt)
k.privateKey = pk
k.apiKey = apiKey
k.rnd = rand.New(s)
return k
}
const timeout = 5 * time.Millisecond
type secrets struct{}
func (s secrets) Seed(seed int64) {}
func (s secrets) Uint64() (r uint64) {
err := binary.Read(crand.Reader, binary.BigEndian, &r)
if err != nil {
log.Error(err)
}
return r
}
func (s secrets) Int63() int64 {
return int64(s.Uint64() & ^uint64(1<<63))
}
func (k *FbKeyMgmt) createAndSignJWTToken(path string, bodyJSON string) (string, error) {
token := &jwt.MapClaims{
"uri": path,
"nonce": k.rnd.Int63(),
"iat": time.Now().Unix(),
"exp": time.Now().Add(time.Second * 55).Unix(),
"sub": k.apiKey,
"bodyHash": createHash(bodyJSON),
}
j := jwt.NewWithClaims(jwt.SigningMethodRS256, token)
signedToken, err := j.SignedString(k.privateKey)
if err != nil {
log.Error(err)
}
return signedToken, err
}
func createHash(data string) string {
h := sha256.New()
h.Write([]byte(data))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}
type FireBlockSDK struct {
httpClient *hystrix.Client
apiBaseURL string
kto *FbKeyMgmt
}
func ParsePrivateKey(privateKeyPEMString []byte) (*rsa.PrivateKey, error) {
return jwt.ParseRSAPrivateKeyFromPEM(privateKeyPEMString)
}
// NewInstance - create new type to handle Fireblocks API requests
// t - timeout for the http client
// pk - private key for signing JWT token
// ak - API key
// url - Fireblocks API URL (https://sandbox-api.fireblocks.io)
func NewInstance(privateK *rsa.PrivateKey, ak string, url string, t time.Duration) *FireBlockSDK {
if t == time.Duration(0) {
// use default
t = timeout
}
s := new(FireBlockSDK)
s.apiBaseURL = url
s.kto = NewInstanceKeyMgmt(privateK, ak)
s.httpClient = newCircuitBreakerHttpClient(t)
return s
}
func newCircuitBreakerHttpClient(t time.Duration) *hystrix.Client {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: false,
}
c := hystrix.NewClient(hystrix.WithHTTPTimeout(t),
hystrix.WithFallbackFunc(func(err error) error {
log.Errorf("no fallback func implemented: %s", err)
return err
}))
return c
}
// getRequest - internal method to handle API call to Fireblocks
func (s *FireBlockSDK) getRequest(path string) (string, error) {
urlEndPoint := s.apiBaseURL + path
token, err := s.kto.CreateAndSignJWTToken(path, "")
if err != nil {
log.Error(err)
return fmt.Sprintf("{message: \"%s.\"}", "error signing JWT token"), err
}
request, err := http.NewRequest(http.MethodGet, urlEndPoint, nil)
if err != nil {
log.Error(err)
return fmt.Sprintf("{message: \"%s.\"}", "error creating NewRequest"), err
}
request.Header.Add("X-API-Key", s.kto.apiKey)
request.Header.Add("Authorization", fmt.Sprintf("Bearer %v", token))
response, err := s.httpClient.Do(request)
if err != nil {
log.Error(err)
return "", err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Error(err)
}
}(response.Body)
data, err := io.ReadAll(response.Body)
if err != nil {
log.Errorf("error communicating with fireblocks: %v", err)
return "", err
}
if response.StatusCode >= 300 {
errMsg := fmt.Sprintf("fireblocks server: %s \n %s", response.Status, string(data))
log.Warning(errMsg)
}
return string(data), err
}
func (s *FireBlockSDK) changeRequest(path string, payload []byte, idempotencyKey string, requestType string) (string, error) {
urlEndPoint := s.apiBaseURL + path
token, err := s.kto.CreateAndSignJWTToken(path, string(payload))
if err != nil {
log.Error(err)
return fmt.Sprintf("{message: \"%s.\"}", "error signing JWT token"), err
}
request, err := http.NewRequest(requestType, urlEndPoint, bytes.NewBuffer(payload))
if err != nil {
log.Error(err)
return fmt.Sprintf("{message: \"%s.\"}", "error creating NewRequest"), err
}
request.Header.Set("X-API-Key", string(s.kto.apiKey))
request.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
request.Header.Set("Content-Type", "application/json")
if len(idempotencyKey) > 0 {
request.Header.Set("Idempotency-Key", idempotencyKey)
}
response, err := s.httpClient.Do(request)
if err != nil {
log.Error(err)
return "", err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Error(err)
}
}(response.Body)
data, err := io.ReadAll(response.Body)
if err != nil {
log.Errorf("error on communicating with Fireblocks: %v \n data: %s", err, data)
return "", err
}
if response.StatusCode >= 300 {
errMsg := fmt.Sprintf("fireblocks server: %s \n %s", response.Status, string(data))
log.Warning(errMsg)
return errMsg, errors.New(errMsg)
}
return string(data), err
}
// CreateVaultAccount
// name - vaultaccount name - usually we use as a join of userid + product_id (XXXX_YYYY)
func (s *FireBlockSDK) CreateVaultAccount(name string, hiddenOnUI bool, customerRefID string, autoFuel bool, idempotencyKey string) (VaultAccount, error) {
payload := map[string]interface{}{
"name": name,
"hiddenOnUI": hiddenOnUI,
"autoFuel": autoFuel,
}
if len(customerRefID) > 0 {
payload["customerRefId"] = customerRefID
}
marshalled, err := json.Marshal(payload)
if err != nil {
return VaultAccount{}, err
}
returnedData, err := s.changeRequest("/vault/accounts", marshalled, idempotencyKey, http.MethodPost)
if err != nil {
log.Error(err)
}
var vaultAccount VaultAccount
err = json.Unmarshal([]byte(returnedData), &vaultAccount)
if err != nil {
log.Error(err)
}
if vaultAccount.Id == "" {
return vaultAccount, errors.New(returnedData)
}
return vaultAccount, err
}
package sdk
import (
"testing"
"github.com/golang-jwt/jwt"
"github.com/stretchr/testify/assert"
)
var testData = []struct {
name string
uri string
bodyJson string
}{
{
name: "With Body",
uri: "v1/hello/hi",
bodyJson: `{"body":"hello"}`,
},
{
name: "Without Body",
uri: "v1/hello/hi",
bodyJson: "",
},
{
name: "With Body and Query Params",
uri: "v1/hello/hi?name=John",
bodyJson: `{"body":"hello hey"}`,
},
}
var rawPrivateKeyBytes = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBANNDx2cqgeZjZ19MLGL7VePFjskSuhzce3ptgOJYBudsXSCaKljG
66IA0wKByctLSNWkUa7BS9wxr7+aOnyLNtECAwEAAQJBAIS3JYLnrybd90hkf9XG
chRePO6Ptx7+Wwtz0u1dwyiJRDaIqkFOAIn6IbBPrmTxk5mEPZUGbWMQJOj62BP8
1AkCIQDwVmj7qHwzsWuGGPwMqJxhFG3ZnbXVpnvicIXlxNWJdwIhAOEIV83vVF64
6hfBx7VrzvmYDMBDdhM4cvUV385dHlP3AiEAuUzWOpnH0Q9M6KIgyx3BHDRlEbC/
/o8S2x6IjgP547cCIF1NnUJYmi3QE9eX1BsnwSCB57+L+RgNDrUJxcsFlv6PAiEA
6kSXtjXeXJ3EcchBjzd7KFf2j/NanymFOrqNWzSOqGA=
-----END RSA PRIVATE KEY-----`)
var apiKey = "nwSCB57+L+RgNDrUJx"
func TestCreateAndSignJWTToken(t *testing.T) {
for _, tt := range testData {
t.Run(tt.name, func(t *testing.T) {
prvtKey, err := ParsePrivateKey(rawPrivateKeyBytes)
if err != nil {
t.Error(err.Error())
}
keyMgmt := NewInstanceKeyMgmt(prvtKey, apiKey)
signedToken, err := keyMgmt.createAndSignJWTToken(tt.uri, tt.bodyJson)
if err != nil {
t.Error(err.Error())
}
// Assertions to validate the signed token
parsedToken, err := jwt.Parse(signedToken, func(token *jwt.Token) (interface{}, error) {
return prvtKey.Public(), nil
})
if err != nil {
t.Error("Error parsing the signed token:", err.Error())
return
}
// Check if the token is valid
if !parsedToken.Valid {
t.Error("The signed token is not valid.")
return
}
// Check if the token has the correct claims
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok {
t.Error("The signed token does not have the correct claims.")
return
}
// Check individual claims
assert.Equal(t, createHash(tt.bodyJson), claims["bodyHash"])
assert.Equal(t, tt.uri, claims["uri"])
assert.Equal(t, apiKey, claims["sub"])
t.Log("Signed token:", signedToken)
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment