From e0883d79f4681796a50c1816d2fe9d8332a523cc Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Tue, 12 Nov 2024 19:53:44 +0200 Subject: [PATCH] Handle document expiration web-hook and updates verification status (#19) * handle document expiration webhook and updates verification status * log parsed clientID * use const to prevents typos when using common headers and query parameters * fix processClientID false positive in suffix validation * log plain requets body not the base64 version * verifiy twinId query paramter early in the handler * prevent logging sensitive data from the Verification struct * use timeout for handling ProcessDocExpirationNotification * implement toOutcome on Verification struct * refactor Verification.ToOutcome --- internal/clients/idenfy/idenfy.go | 1 + internal/handlers/handlers.go | 81 ++++++++---- internal/middleware/middleware.go | 13 +- internal/models/doc_expiration.go | 17 +++ internal/models/verification.go | 122 +++++++++++++----- internal/repository/mongo.go | 1 + .../repository/verification_repository.go | 21 +++ internal/server/server.go | 10 +- internal/services/services.go | 92 +++++++------ 9 files changed, 258 insertions(+), 100 deletions(-) create mode 100644 internal/models/doc_expiration.go diff --git a/internal/clients/idenfy/idenfy.go b/internal/clients/idenfy/idenfy.go index fbe1911..830869f 100644 --- a/internal/clients/idenfy/idenfy.go +++ b/internal/clients/idenfy/idenfy.go @@ -158,5 +158,6 @@ func (c *Idenfy) createVerificationSessionRequestBody(clientID string, devMode b RequestBody.ExpiryTime = TokenExpiryDevModeSeconds RequestBody.DummyStatus = "APPROVED" } + c.logger.Debug("Creating verification session", "request", RequestBody) return RequestBody } diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 30aa72d..7f74c0c 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -9,11 +9,10 @@ This layer is responsible for handling the requests and responses, in more detai package handlers import ( - "bytes" "context" - "encoding/json" "fmt" "log/slog" + "strconv" "time" "github.com/gofiber/fiber/v2" @@ -28,6 +27,20 @@ import ( "github.com/threefoldtech/tf-kyc-verifier/internal/services" ) +const ( + // Authentication headers + HeaderClientID = "X-Client-ID" + + // iDenfy webhook headers + HeaderIdenfySignature = "Idenfy-Signature" + + // Query parameters + QueryParamClientID = "client_id" + QueryParamTwinID = "twin_id" + + HandlerTimeout = 5 * time.Second +) + type Handler struct { kycService *services.KYCService config *config.Config @@ -66,8 +79,8 @@ func NewHandler(kycService *services.KYCService, config *config.Config, logger * // @Router /api/v1/token [post] func (h *Handler) GetOrCreateVerificationToken() fiber.Handler { return func(c *fiber.Ctx) error { - clientID := c.Get("X-Client-ID") - ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) + clientID := c.Get(HeaderClientID) + ctx, cancel := context.WithTimeout(c.Context(), HandlerTimeout) defer cancel() token, isNewToken, err := h.kycService.GetOrCreateVerificationToken(ctx, clientID) if err != nil { @@ -97,8 +110,8 @@ func (h *Handler) GetOrCreateVerificationToken() fiber.Handler { // @Router /api/v1/data [get] func (h *Handler) GetVerificationData() fiber.Handler { return func(c *fiber.Ctx) error { - clientID := c.Get("X-Client-ID") - ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) + clientID := c.Get(HeaderClientID) + ctx, cancel := context.WithTimeout(c.Context(), HandlerTimeout) defer cancel() verification, err := h.kycService.GetVerificationData(ctx, clientID) if err != nil { @@ -127,8 +140,8 @@ func (h *Handler) GetVerificationData() fiber.Handler { // @Router /api/v1/status [get] func (h *Handler) GetVerificationStatus() fiber.Handler { return func(c *fiber.Ctx) error { - clientID := c.Query("client_id") - twinID := c.Query("twin_id") + clientID := c.Query(QueryParamClientID) + twinID := c.Query(QueryParamTwinID) if clientID == "" && twinID == "" { h.logger.Warn("Bad request: missing client_id and twin_id") @@ -136,12 +149,17 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { } var verification *models.VerificationOutcome var err error - ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) + ctx, cancel := context.WithTimeout(c.Context(), HandlerTimeout) defer cancel() if clientID != "" { verification, err = h.kycService.GetVerificationStatus(ctx, clientID) } else { - verification, err = h.kycService.GetVerificationStatusByTwinID(ctx, twinID) + twinIDUint64, parseErr := strconv.ParseUint(twinID, 10, 32) + if parseErr != nil { + h.logger.Error("Error parsing twinID", "twinID", twinID, "error", parseErr) + return responses.RespondWithError(c, fiber.StatusBadRequest, fmt.Errorf("invalid twinID")) + } + verification, err = h.kycService.GetVerificationStatusByTwinID(ctx, uint32(twinIDUint64)) } if err != nil { h.logger.Error("Failed to get verification status", "clientID", clientID, "twinID", twinID, "error", err) @@ -166,26 +184,22 @@ func (h *Handler) GetVerificationStatus() fiber.Handler { func (h *Handler) ProcessVerificationResult() fiber.Handler { return func(c *fiber.Ctx) error { h.logger.Debug("Received verification update", - "body", string(c.Body()), "headers", &c.Request().Header, ) - sigHeader := c.Get("Idenfy-Signature") + sigHeader := c.Get(HeaderIdenfySignature) if len(sigHeader) < 1 { + h.logger.Error("Missing signature header", "headers", string(c.Request().Header.Header())) return responses.RespondWithError(c, fiber.StatusBadRequest, fmt.Errorf("no signature provided")) } body := c.Body() var result models.Verification - decoder := json.NewDecoder(bytes.NewReader(body)) - err := decoder.Decode(&result) - if err != nil { + if err := c.BodyParser(&result); err != nil { h.logger.Error("Error decoding verification update", "error", err) return responses.RespondWithError(c, fiber.StatusBadRequest, err) } - h.logger.Debug("Verification update after decoding", "result", result) - ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) + ctx, cancel := context.WithTimeout(c.Context(), HandlerTimeout) defer cancel() - err = h.kycService.ProcessVerificationResult(ctx, body, sigHeader, result) - if err != nil { + if err := h.kycService.ProcessVerificationResult(ctx, body, sigHeader, result); err != nil { return HandleError(c, err) } return responses.RespondWithData(c, fiber.StatusOK, nil) @@ -201,9 +215,30 @@ func (h *Handler) ProcessVerificationResult() fiber.Handler { // @Router /webhooks/idenfy/id-expiration [post] func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { return func(c *fiber.Ctx) error { - // TODO: implement - h.logger.Error("Received ID expiration notification but not implemented") - return c.SendStatus(fiber.StatusNotImplemented) + h.logger.Debug("Received ID expiration update", + "body", string(c.Body()), + "headers", &c.Request().Header, + ) + + // Verify signature + sigHeader := c.Get(HeaderIdenfySignature) + if len(sigHeader) < 1 { + h.logger.Error("Missing signature header", "headers", string(c.Request().Header.Header())) + return responses.RespondWithError(c, fiber.StatusBadRequest, fmt.Errorf("missing signature header")) + } + body := c.Body() + var notification models.DocExpirationNotification + if err := c.BodyParser(¬ification); err != nil { + h.logger.Error("Error decoding verification update", "error", err) + return responses.RespondWithError(c, fiber.StatusBadRequest, fmt.Errorf("invalid request body")) + } + ctx, cancel := context.WithTimeout(c.Context(), HandlerTimeout) + defer cancel() + if err := h.kycService.ProcessDocExpirationNotification(ctx, body, sigHeader, notification); err != nil { + return HandleError(c, err) + } + + return c.SendStatus(fiber.StatusOK) } } @@ -214,7 +249,7 @@ func (h *Handler) ProcessDocExpirationNotification() fiber.Handler { // @Router /api/v1/health [get] func (h *Handler) HealthCheck(dbClient *mongo.Client) fiber.Handler { return func(c *fiber.Ctx) error { - ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second) + ctx, cancel := context.WithTimeout(c.Context(), HandlerTimeout) defer cancel() err := dbClient.Ping(ctx, readpref.Primary()) if err != nil { diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go index f70107d..491bcc0 100644 --- a/internal/middleware/middleware.go +++ b/internal/middleware/middleware.go @@ -17,12 +17,19 @@ import ( "github.com/vedhavyas/go-subkey/v2/sr25519" ) +const ( + // Authentication headers + HeaderClientID = "X-Client-ID" + HeaderChallenge = "X-Challenge" + HeaderSignature = "X-Signature" +) + // AuthMiddleware is a middleware that validates the authentication credentials func AuthMiddleware(config config.Challenge) fiber.Handler { return func(c *fiber.Ctx) error { - clientID := c.Get("X-Client-ID") - signature := c.Get("X-Signature") - challenge := c.Get("X-Challenge") + clientID := c.Get(HeaderClientID) + signature := c.Get(HeaderSignature) + challenge := c.Get(HeaderChallenge) if clientID == "" || signature == "" || challenge == "" { return responses.RespondWithError(c, fiber.StatusBadRequest, fmt.Errorf("missing authentication credentials")) diff --git a/internal/models/doc_expiration.go b/internal/models/doc_expiration.go new file mode 100644 index 0000000..924b8b3 --- /dev/null +++ b/internal/models/doc_expiration.go @@ -0,0 +1,17 @@ +package models + +type ExpirationThreshold string + +const ( + ExpiresWithin30Days ExpirationThreshold = "DOCUMENT_EXPIRES_WITHIN_30_DAYS" + ExpiresWithin7Days ExpirationThreshold = "DOCUMENT_EXPIRES_WITHIN_7_DAYS" + ExpiresWithin1Day ExpirationThreshold = "DOCUMENT_EXPIRES_WITHIN_1_DAY" + DocumentExpired ExpirationThreshold = "DOCUMENT_EXPIRED" +) + +type DocExpirationNotification struct { + ScanRef string `json:"scanRef"` + ClientID string `json:"clientId"` + ExpirationThreshold ExpirationThreshold `json:"expirationThreshold"` + DocumentExpiration string `json:"documentExpiration"` +} diff --git a/internal/models/verification.go b/internal/models/verification.go index 7320f1d..15e0652 100644 --- a/internal/models/verification.go +++ b/internal/models/verification.go @@ -1,39 +1,98 @@ package models import ( + "log/slog" "time" + "github.com/threefoldtech/tf-kyc-verifier/internal/config" "go.mongodb.org/mongo-driver/bson/primitive" ) type Verification struct { - ID primitive.ObjectID `bson:"_id,omitempty" json:"-"` - CreatedAt time.Time `bson:"createdAt" json:"-"` - Final *bool `bson:"final" json:"final"` // required - Platform Platform `bson:"platform" json:"platform"` // required - Status Status `bson:"status" json:"status"` // required - Data PersonData `bson:"data" json:"data"` // required - FileUrls map[string]string `bson:"fileUrls" json:"fileUrls"` // required - IdenfyRef string `bson:"scanRef" json:"scanRef"` // required - ClientID string `bson:"clientId" json:"clientId"` // required - StartTime int64 `bson:"startTime" json:"startTime"` // required - FinishTime int64 `bson:"finishTime" json:"finishTime"` // required - ClientIP string `bson:"clientIp" json:"clientIp"` // required - ClientIPCountry string `bson:"clientIpCountry" json:"clientIpCountry"` // required - ClientLocation string `bson:"clientLocation" json:"clientLocation"` // required - CompanyID string `bson:"companyId" json:"companyId"` // required - BeneficiaryID string `bson:"beneficiaryId" json:"beneficiaryId"` // required - RegistryCenterCheck interface{} `json:"registryCenterCheck,omitempty"` - AddressVerification interface{} `json:"addressVerification,omitempty"` - QuestionnaireAnswers interface{} `json:"questionnaireAnswers,omitempty"` - AdditionalSteps map[string]string `json:"additionalSteps,omitempty"` - UtilityData []string `json:"utilityData,omitempty"` - AdditionalStepPdfUrls map[string]string `json:"additionalStepPdfUrls,omitempty"` - AML []AMLCheck `bson:"AML" json:"AML,omitempty"` - LID []LID `bson:"LID" json:"LID,omitempty"` - ExternalRef string `bson:"externalRef" json:"externalRef,omitempty"` - ManualAddress string `bson:"manualAddress" json:"manualAddress,omitempty"` - ManualAddressMatch *bool `bson:"manualAddressMatch" json:"manualAddressMatch,omitempty"` + ID primitive.ObjectID `bson:"_id,omitempty" json:"-"` + CreatedAt time.Time `bson:"createdAt" json:"-"` + Final *bool `bson:"final" json:"final"` // required + Platform Platform `bson:"platform" json:"platform"` // required + Status Status `bson:"status" json:"status"` // required + Data PersonData `bson:"data" json:"data"` // required + FileUrls map[string]string `bson:"fileUrls" json:"fileUrls"` // required + IdenfyRef string `bson:"scanRef" json:"scanRef"` // required + ClientID string `bson:"clientId" json:"clientId"` // required + StartTime int64 `bson:"startTime" json:"startTime"` // required + FinishTime int64 `bson:"finishTime" json:"finishTime"` // required + ClientIP string `bson:"clientIp" json:"clientIp"` // required + ClientIPCountry string `bson:"clientIpCountry" json:"clientIpCountry"` // required + ClientLocation string `bson:"clientLocation" json:"clientLocation"` // required + CompanyID string `bson:"companyId" json:"companyId"` // required + BeneficiaryID string `bson:"beneficiaryId" json:"beneficiaryId"` // required + RegistryCenterCheck interface{} `json:"registryCenterCheck,omitempty"` + AddressVerification interface{} `json:"addressVerification,omitempty"` + QuestionnaireAnswers interface{} `json:"questionnaireAnswers,omitempty"` + AdditionalSteps map[string]string `json:"additionalSteps,omitempty"` + UtilityData []string `json:"utilityData,omitempty"` + AdditionalStepPdfUrls map[string]string `json:"additionalStepPdfUrls,omitempty"` + AML []AMLCheck `bson:"AML" json:"AML,omitempty"` + LID []LID `bson:"LID" json:"LID,omitempty"` + ExternalRef string `bson:"externalRef" json:"externalRef,omitempty"` + ManualAddress string `bson:"manualAddress" json:"manualAddress,omitempty"` + ManualAddressMatch *bool `bson:"manualAddressMatch" json:"manualAddressMatch,omitempty"` + ExpirationStatus *ExpirationThreshold `bson:"expirationStatus,omitempty" json:"expirationStatus,omitempty"` +} + +// implements slog.LogValuer to control how Verification is logged +func (v Verification) LogValue() slog.Value { + // Create a copy without sensitive data + sanitized := &Verification{ + Final: v.Final, + Platform: v.Platform, + Status: v.Status, + IdenfyRef: v.IdenfyRef, + ClientID: v.ClientID, + StartTime: v.StartTime, + FinishTime: v.FinishTime, + ExpirationStatus: v.ExpirationStatus, + } + + // Convert to a map for logging + return slog.GroupValue( + slog.Any("final", sanitized.Final), + slog.String("platform", string(sanitized.Platform)), + slog.Any("status", sanitized.Status), + slog.String("idenfyRef", sanitized.IdenfyRef), + slog.String("clientId", sanitized.ClientID), + slog.Int64("startTime", sanitized.StartTime), + slog.Int64("finishTime", sanitized.FinishTime), + slog.Any("expirationStatus", sanitized.ExpirationStatus), + ) +} + +// ToOutcome converts a Verification to a VerificationOutcome based on the service configuration +func (v Verification) ToOutcome(config config.Verification) *VerificationOutcome { + outcome := OutcomeRejected + // First check if Overall status exists + if v.Status.Overall != nil { + // Then evaluate if it's either: + // 1. Overall status is Approved, or + // 2. Overall status is Suspected AND config allows suspicious cases to be approved + if *v.Status.Overall == OverallApproved || + (*v.Status.Overall == OverallSuspected && config.SuspiciousVerificationOutcome == "APPROVED") { + + // If either condition is met, set outcome to Approved + outcome = OutcomeApproved + + // Finally check if document is expired - this overrides the Approved outcome based on config + if v.ExpirationStatus != nil && *v.ExpirationStatus == DocumentExpired { + outcome = Outcome(config.ExpiredDocumentOutcome) + } + } + } + return &VerificationOutcome{ + Final: v.Final, + ClientID: v.ClientID, + IdenfyRef: v.IdenfyRef, + ExpirationThreshold: v.ExpirationStatus, + Outcome: outcome, + } } type Platform string @@ -237,10 +296,11 @@ type ServiceStatus struct { } type VerificationOutcome struct { - Final *bool `bson:"final"` - ClientID string `bson:"clientId"` - IdenfyRef string `bson:"idenfyRef"` - Outcome Outcome `bson:"outcome"` + Final *bool `bson:"final"` + ClientID string `bson:"clientId"` + IdenfyRef string `bson:"idenfyRef"` + ExpirationThreshold *ExpirationThreshold `bson:"expirationThreshold"` + Outcome Outcome `bson:"outcome"` } type Outcome string diff --git a/internal/repository/mongo.go b/internal/repository/mongo.go index 1206271..099962c 100644 --- a/internal/repository/mongo.go +++ b/internal/repository/mongo.go @@ -18,6 +18,7 @@ type TokenRepository interface { type VerificationRepository interface { SaveVerification(ctx context.Context, verification *models.Verification) error GetVerification(ctx context.Context, clientID string) (*models.Verification, error) + UpdateExpirationStatus(ctx context.Context, clientID string, scanRef string, status models.ExpirationThreshold) error } func NewMongoClient(ctx context.Context, mongoURI string) (*mongo.Client, error) { diff --git a/internal/repository/verification_repository.go b/internal/repository/verification_repository.go index 3b1d203..74c9b95 100644 --- a/internal/repository/verification_repository.go +++ b/internal/repository/verification_repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" "log/slog" "time" @@ -56,3 +57,23 @@ func (r *MongoVerificationRepository) GetVerification(ctx context.Context, clien } return &verification, nil } + +func (r *MongoVerificationRepository) UpdateExpirationStatus(ctx context.Context, clientID string, scanRef string, status models.ExpirationThreshold) error { + filter := bson.M{"clientId": clientID, "scanRef": scanRef} + update := bson.M{ + "$set": bson.M{ + "expirationStatus": status, + }, + } + + result, err := r.collection.UpdateOne(ctx, filter, update) + if err != nil { + return fmt.Errorf("updating expiration status: %w", err) + } + + if result.MatchedCount == 0 { + return fmt.Errorf("verification not found for client: %s", clientID) + } + + return nil +} diff --git a/internal/server/server.go b/internal/server/server.go index 1b79c7e..d3d2de3 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -91,7 +91,7 @@ func (s *Server) initializeCore(ctx context.Context) error { } // Setup database - dbClient, db, err := s.setupDatabase(ctx) + db, err := s.setupDatabase(ctx) if err != nil { return fmt.Errorf("setting up database: %w", err) } @@ -109,7 +109,7 @@ func (s *Server) initializeCore(ctx context.Context) error { } // Setup routes - if err := s.setupRoutes(service, dbClient); err != nil { + if err := s.setupRoutes(service, db.Client()); err != nil { return fmt.Errorf("setting up routes: %w", err) } @@ -176,15 +176,15 @@ func (s *Server) setupMiddleware() error { return nil } -func (s *Server) setupDatabase(ctx context.Context) (*mongo.Client, *mongo.Database, error) { +func (s *Server) setupDatabase(ctx context.Context) (*mongo.Database, error) { s.logger.Debug("Connecting to database") client, err := repository.NewMongoClient(ctx, s.config.MongoDB.URI) if err != nil { - return nil, nil, fmt.Errorf("setting up database: %w", err) + return nil, fmt.Errorf("setting up database: %w", err) } - return client, client.Database(s.config.MongoDB.DatabaseName), nil + return client.Database(s.config.MongoDB.DatabaseName), nil } type repositories struct { diff --git a/internal/services/services.go b/internal/services/services.go index 24c07ca..9ebc2a4 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -9,7 +9,6 @@ import ( "fmt" "log/slog" "slices" - "strconv" "strings" "time" @@ -168,32 +167,15 @@ func (s *KYCService) GetVerificationStatus(ctx context.Context, clientID string) s.logger.Error("Error getting verification from database", "clientID", clientID, "error", err) return nil, errors.NewInternalError("getting verification from database", err) } - var outcome models.Outcome - if verification != nil { - if verification.Status.Overall != nil && *verification.Status.Overall == models.OverallApproved || (s.config.SuspiciousVerificationOutcome == "APPROVED" && *verification.Status.Overall == models.OverallSuspected) { - outcome = models.OutcomeApproved - } else { - outcome = models.OutcomeRejected - } - } else { + if verification == nil { return nil, nil } - return &models.VerificationOutcome{ - Final: verification.Final, - ClientID: clientID, - IdenfyRef: verification.IdenfyRef, - Outcome: outcome, - }, nil + return verification.ToOutcome(*s.config), nil } -func (s *KYCService) GetVerificationStatusByTwinID(ctx context.Context, twinID string) (*models.VerificationOutcome, error) { +func (s *KYCService) GetVerificationStatusByTwinID(ctx context.Context, twinID uint32) (*models.VerificationOutcome, error) { // get the address from the twinID - twinIDUint64, err := strconv.ParseUint(twinID, 10, 32) - if err != nil { - s.logger.Error("Error parsing twinID", "twinID", twinID, "error", err) - return nil, errors.NewInternalError("parsing twinID", err) - } - address, err := s.substrate.GetAddressByTwinID(uint32(twinIDUint64)) + address, err := s.substrate.GetAddressByTwinID(twinID) if err != nil { s.logger.Error("Error getting address from twinID", "twinID", twinID, "error", err) return nil, errors.NewExternalError("looking up twinID address from TFChain", err) @@ -202,23 +184,16 @@ func (s *KYCService) GetVerificationStatusByTwinID(ctx context.Context, twinID s } func (s *KYCService) ProcessVerificationResult(ctx context.Context, body []byte, sigHeader string, result models.Verification) error { - err := s.idenfy.VerifyCallbackSignature(ctx, body, sigHeader) + err := s.verifyIdenfyCallbackSignature(ctx, body, sigHeader) if err != nil { - s.logger.Error("Error verifying callback signature", "sigHeader", sigHeader, "error", err) - return errors.NewAuthorizationError("verifying callback signature", err) + return err } - clientIDParts := strings.Split(result.ClientID, ":") - if len(clientIDParts) < 2 { - s.logger.Error("clientID have no network suffix", "clientID", result.ClientID) - return errors.NewInternalError("invalid clientID", nil) - } - networkSuffix := clientIDParts[len(clientIDParts)-1] - if networkSuffix != s.IdenfySuffix { - s.logger.Error("clientID has different network suffix", "clientID", result.ClientID, "expectedSuffix", s.IdenfySuffix, "actualSuffix", networkSuffix) - return errors.NewInternalError("invalid clientID", nil) + clientID, err := s.processClientID(result.ClientID) + if err != nil { + return err } // delete the token with the same clientID and same scanRef - result.ClientID = clientIDParts[0] + result.ClientID = clientID err = s.tokenRepo.DeleteToken(ctx, result.ClientID, result.IdenfyRef) if err != nil { @@ -233,14 +208,55 @@ func (s *KYCService) ProcessVerificationResult(ctx context.Context, body []byte, return errors.NewInternalError("saving verification to database", err) } } - s.logger.Debug("Verification result processed successfully", "result", result) + s.logger.Info("Verification result processed successfully", "result", result) return nil } -func (s *KYCService) ProcessDocExpirationNotification(ctx context.Context, clientID string) error { +func (s *KYCService) ProcessDocExpirationNotification(ctx context.Context, body []byte, sigHeader string, notification models.DocExpirationNotification) error { + err := s.verifyIdenfyCallbackSignature(ctx, body, sigHeader) + if err != nil { + return err + } + clientID, err := s.processClientID(notification.ClientID) + if err != nil { + return err + } + // Update verification that matches the same clientID and scanref with expiration status + err = s.verificationRepo.UpdateExpirationStatus(ctx, clientID, notification.ScanRef, notification.ExpirationThreshold) + if err != nil { + s.logger.Error("Error updating expiration status", + "clientID", clientID, + "status", notification.ExpirationThreshold, + "error", err) + return errors.NewInternalError("updating expiration status", err) + } + + s.logger.Info("Updated document expiration status", + "clientID", clientID, + "status", notification.ExpirationThreshold) return nil } +func (s *KYCService) verifyIdenfyCallbackSignature(ctx context.Context, body []byte, sigHeader string) error { + err := s.idenfy.VerifyCallbackSignature(ctx, body, sigHeader) + if err != nil { + s.logger.Error("Error verifying callback signature", "sigHeader", sigHeader, "error", err) + return errors.NewAuthorizationError("verifying callback signature", err) + } + return nil +} + +func (s *KYCService) processClientID(clientID string) (string, error) { + strippedClientID, actualSuffix, found := strings.Cut(clientID, ":") + // defensively check if the clientID has a network suffix that is different from the expected one + if !found { + s.logger.Warn("clientID have no network suffix", "clientID", clientID) + } else if actualSuffix != s.IdenfySuffix { + s.logger.Warn("clientID has different network suffix", "clientID", clientID, "expectedSuffix", s.IdenfySuffix, "actualSuffix", actualSuffix) + } + return strippedClientID, nil +} + func (s *KYCService) IsUserVerified(ctx context.Context, clientID string) (bool, error) { verification, err := s.verificationRepo.GetVerification(ctx, clientID) if err != nil { @@ -250,5 +266,5 @@ func (s *KYCService) IsUserVerified(ctx context.Context, clientID string) (bool, if verification == nil { return false, nil } - return verification.Status.Overall != nil && (*verification.Status.Overall == models.OverallApproved || (s.config.SuspiciousVerificationOutcome == "APPROVED" && *verification.Status.Overall == models.OverallSuspected)), nil + return verification.ToOutcome(*s.config).Outcome == models.OutcomeApproved, nil }