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

Refactor idenfy client to use net/http instead of fasthttp #18

Merged
merged 2 commits into from
Nov 12, 2024
Merged
Changes from 1 commit
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
147 changes: 94 additions & 53 deletions internal/clients/idenfy/idenfy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
}