From e1640874d1423ba8a2695a5e8278b6047880975e Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Sun, 10 Nov 2024 15:11:13 +0200 Subject: [PATCH 1/2] refactor idenfy client to use net/http instead of fasthttp iss#8 --- internal/clients/idenfy/idenfy.go | 147 +++++++++++++++++++----------- 1 file changed, 94 insertions(+), 53 deletions(-) diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index 91d26e3..fbe1911 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -7,6 +7,7 @@ This layer is responsible for interacting with the iDenfy API. the main operatio package idenfy import ( + "bytes" "context" "crypto/hmac" "crypto/sha256" @@ -15,107 +16,147 @@ import ( "encoding/json" "errors" "fmt" + "io" "log/slog" + "net/http" "time" "github.com/threefoldtech/tf-kyc-verifier/internal/models" - "github.com/valyala/fasthttp" ) type Idenfy struct { - client *fasthttp.Client // TODO: Interface - config IdenfyConfig // TODO: Interface + client *http.Client + config IdenfyConfig logger *slog.Logger } const ( VerificationSessionEndpoint = "/api/v2/token" + TokenExpirySeconds = 86400 + TokenExpiryDevModeSeconds = 30 + DefaultTimeout = 10 * time.Second + ContentTypeJSON = "application/json" ) func New(config IdenfyConfig, logger *slog.Logger) *Idenfy { return &Idenfy{ - client: &fasthttp.Client{}, + client: &http.Client{ + Timeout: DefaultTimeout, + }, config: config, logger: logger, } } -func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) (models.Token, error) { // TODO: Refactor - url := c.config.GetBaseURL() + VerificationSessionEndpoint +func (c *Idenfy) CreateVerificationSession(ctx context.Context, clientID string) (models.Token, error) { + req, err := c.prepareRequest(ctx, clientID, TokenExpirySeconds) + if err != nil { + return models.Token{}, fmt.Errorf("preparing request: %w", err) + } - req := fasthttp.AcquireRequest() - defer fasthttp.ReleaseRequest(req) + resp, err := c.client.Do(req) + if err != nil { + return models.Token{}, fmt.Errorf("sending request: %w", err) + } + defer resp.Body.Close() - req.SetRequestURI(url) - req.Header.SetMethod(fasthttp.MethodPost) - req.Header.Set("Content-Type", "application/json") + return c.handleResponse(resp) +} - // Set basic auth - authStr := c.config.GetAPIKey() + ":" + c.config.GetAPISecret() - auth := base64.StdEncoding.EncodeToString([]byte(authStr)) - req.Header.Set("Authorization", "Basic "+auth) +func (c *Idenfy) VerifyCallbackSignature(ctx context.Context, body []byte, sigHeader string) error { + sig, err := hex.DecodeString(sigHeader) + if err != nil { + return fmt.Errorf("invalid signature format: %w", err) + } + mac := hmac.New(sha256.New, []byte(c.config.GetCallbackSignKey())) + mac.Write(body) - RequestBody := c.createVerificationSessionRequestBody(clientID, c.config.GetDevMode()) + if !hmac.Equal(sig, mac.Sum(nil)) { + return errors.New("signature verification failed") + } + return nil +} - jsonBody, err := json.Marshal(RequestBody) +func (c *Idenfy) prepareRequest(ctx context.Context, clientID string, tokenExpiryTime int) (*http.Request, error) { + body, err := c.createRequestBody(clientID, tokenExpiryTime) if err != nil { - return models.Token{}, fmt.Errorf("marshaling request body: %w", err) + return nil, fmt.Errorf("creating request body: %w", err) } - req.SetBody(jsonBody) - // Set deadline from context - deadline, ok := ctx.Deadline() - if ok { - req.SetTimeout(time.Until(deadline)) + + url := c.config.GetBaseURL() + VerificationSessionEndpoint + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) } - resp := fasthttp.AcquireResponse() - defer fasthttp.ReleaseResponse(resp) - c.logger.Debug("Preparing iDenfy verification session request", "request", jsonBody) - err = c.client.Do(req, resp) + c.setRequestHeaders(req) + return req, nil +} + +func (c *Idenfy) createRequestBody(clientID string, tokenExpiryTime int) ([]byte, error) { + requestBody := c.createVerificationSessionRequestBody(clientID, c.config.GetDevMode(), tokenExpiryTime) + jsonBody, err := json.Marshal(requestBody) if err != nil { - return models.Token{}, fmt.Errorf("sending token request to iDenfy: %w", err) + return nil, fmt.Errorf("marshaling request body: %w", err) + } + return jsonBody, nil +} + +func (c *Idenfy) setRequestHeaders(req *http.Request) { + req.Header.Set("Content-Type", ContentTypeJSON) + authStr := c.config.GetAPIKey() + ":" + c.config.GetAPISecret() + auth := base64.StdEncoding.EncodeToString([]byte(authStr)) + req.Header.Set("Authorization", "Basic "+auth) +} + +func (c *Idenfy) handleResponse(resp *http.Response) (models.Token, error) { + if err := c.validateResponseStatus(resp); err != nil { + return models.Token{}, err } - if resp.StatusCode() < 200 || resp.StatusCode() >= 300 { - c.logger.Debug("Received unexpected status code from iDenfy", "status", resp.StatusCode(), "error", string(resp.Body())) - return models.Token{}, fmt.Errorf("unexpected status code from iDenfy: %d", resp.StatusCode()) + body, err := io.ReadAll(resp.Body) + if err != nil { + return models.Token{}, fmt.Errorf("reading response body: %w", err) } - c.logger.Debug("Received response from iDenfy", "response", string(resp.Body())) var result models.Token - if err := json.Unmarshal(resp.Body(), &result); err != nil { + if err := json.Unmarshal(body, &result); err != nil { return models.Token{}, fmt.Errorf("decoding token response from iDenfy: %w", err) } return result, nil } -// verify signature of the callback -func (c *Idenfy) VerifyCallbackSignature(ctx context.Context, body []byte, sigHeader string) error { - sig, err := hex.DecodeString(sigHeader) - if err != nil { - return err - } - mac := hmac.New(sha256.New, []byte(c.config.GetCallbackSignKey())) - - mac.Write(body) - - if !hmac.Equal(sig, mac.Sum(nil)) { - return errors.New("signature verification failed") +func (c *Idenfy) validateResponseStatus(resp *http.Response) error { + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + c.logger.Debug("Received unexpected status code from iDenfy", + "status", resp.StatusCode, + "error", string(body), + ) + return fmt.Errorf("unexpected status code from iDenfy: code: %d, body: %s", resp.StatusCode, string(body)) } return nil } -// function to create a request body for the verification session -func (c *Idenfy) createVerificationSessionRequestBody(clientID string, devMode bool) map[string]interface{} { - RequestBody := map[string]interface{}{ - "clientId": clientID, - "generateDigitString": true, - "callbackUrl": c.config.GetCallbackUrl(), +type VerificationSessionRequest struct { + ClientID string `json:"clientId"` + GenerateDigitString bool `json:"generateDigitString"` + CallbackURL string `json:"callbackUrl"` + ExpiryTime int `json:"expiryTime"` + DummyStatus string `json:"dummyStatus,omitempty"` +} + +func (c *Idenfy) createVerificationSessionRequestBody(clientID string, devMode bool, tokenExpiryTime int) *VerificationSessionRequest { + RequestBody := &VerificationSessionRequest{ + ClientID: clientID, + GenerateDigitString: true, + CallbackURL: c.config.GetCallbackUrl(), + ExpiryTime: tokenExpiryTime, } if devMode { - RequestBody["expiryTime"] = 30 - RequestBody["dummyStatus"] = "APPROVED" + RequestBody.ExpiryTime = TokenExpiryDevModeSeconds + RequestBody.DummyStatus = "APPROVED" } return RequestBody } From 6b9fec3f28fdee5db951274cf7465750732f844c Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Tue, 12 Nov 2024 17:21:11 +0200 Subject: [PATCH 2/2] remove fasthttp direct dependencie from go mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 308aab7..04acdfa 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/stretchr/testify v1.9.0 github.com/swaggo/swag v1.16.3 github.com/threefoldtech/tfchain/clients/tfchain-client-go v0.0.0-20241007205731-5e76664a3cc4 - github.com/valyala/fasthttp v1.51.0 github.com/vedhavyas/go-subkey/v2 v2.0.0 go.mongodb.org/mongo-driver v1.17.1 ) @@ -61,6 +60,7 @@ require ( github.com/swaggo/files/v2 v2.0.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect github.com/vedhavyas/go-subkey v1.0.3 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect