Last active
January 18, 2024 16:45
-
-
Save IamNator/80c32850240f97497464a90a67432212 to your computer and use it in GitHub Desktop.
fireblock-auth
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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