Files
idv/provider/onfido.go

269 lines
6.8 KiB
Go

// Copyright 2024-2026 Lux Partners Limited. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package provider
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const (
OnfidoAPIv3 = "https://api.onfido.com/v3.6"
OnfidoSandboxAPIv3 = "https://api.onfido.com/v3.6"
)
// OnfidoConfig holds Onfido API credentials.
type OnfidoConfig struct {
BaseURL string
APIToken string
WebhookToken string // for webhook signature verification
}
// Onfido implements the Provider interface for Onfido API v3.6.
type Onfido struct {
cfg OnfidoConfig
client *http.Client
}
// NewOnfido creates an Onfido IDV provider.
func NewOnfido(cfg OnfidoConfig) *Onfido {
if cfg.BaseURL == "" {
cfg.BaseURL = OnfidoSandboxAPIv3
}
return &Onfido{
cfg: cfg,
client: &http.Client{Timeout: 30 * time.Second},
}
}
func (o *Onfido) Name() string { return ProviderOnfido }
// InitiateVerification creates an Onfido applicant and starts a check workflow.
func (o *Onfido) InitiateVerification(ctx context.Context, req *VerificationRequest) (*VerificationResponse, error) {
// Step 1: Create applicant
applicant := map[string]interface{}{
"first_name": req.GivenName,
"last_name": req.FamilyName,
"email": req.Email,
}
if req.DateOfBirth != "" {
applicant["dob"] = req.DateOfBirth
}
applicantID, err := o.createApplicant(ctx, applicant)
if err != nil {
return nil, err
}
// Step 2: Create SDK token for client-side integration
sdkToken, err := o.createSDKToken(ctx, applicantID)
if err != nil {
return nil, err
}
// Step 3: Create check (document + facial similarity + optional watchlist)
checkID, err := o.createCheck(ctx, applicantID, req.Workflow)
if err != nil {
return nil, err
}
return &VerificationResponse{
VerificationID: checkID,
Provider: ProviderOnfido,
Status: StatusPending,
RedirectURL: sdkToken, // SDK token for Onfido Web SDK
CreatedAt: time.Now(),
}, nil
}
func (o *Onfido) createApplicant(ctx context.Context, applicant map[string]interface{}) (string, error) {
body, _ := json.Marshal(applicant)
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
o.cfg.BaseURL+"/applicants", bytes.NewReader(body))
if err != nil {
return "", err
}
o.setHeaders(req)
resp, err := o.client.Do(req)
if err != nil {
return "", fmt.Errorf("onfido create applicant: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errBody, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("onfido create applicant %d: %s", resp.StatusCode, string(errBody))
}
var result struct {
ID string `json:"id"`
}
json.NewDecoder(resp.Body).Decode(&result)
return result.ID, nil
}
func (o *Onfido) createSDKToken(ctx context.Context, applicantID string) (string, error) {
body, _ := json.Marshal(map[string]string{
"applicant_id": applicantID,
"referrer": "*://*/*",
})
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
o.cfg.BaseURL+"/sdk_token", bytes.NewReader(body))
if err != nil {
return "", err
}
o.setHeaders(req)
resp, err := o.client.Do(req)
if err != nil {
return "", fmt.Errorf("onfido create SDK token: %w", err)
}
defer resp.Body.Close()
var result struct {
Token string `json:"token"`
}
json.NewDecoder(resp.Body).Decode(&result)
return result.Token, nil
}
func (o *Onfido) createCheck(ctx context.Context, applicantID, workflow string) (string, error) {
reportNames := []string{"document", "facial_similarity_photo"}
if workflow == "enhanced" {
reportNames = append(reportNames, "watchlist_enhanced")
}
checkReq := map[string]interface{}{
"applicant_id": applicantID,
"report_names": reportNames,
}
body, _ := json.Marshal(checkReq)
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
o.cfg.BaseURL+"/checks", bytes.NewReader(body))
if err != nil {
return "", err
}
o.setHeaders(req)
resp, err := o.client.Do(req)
if err != nil {
return "", fmt.Errorf("onfido create check: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errBody, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("onfido create check %d: %s", resp.StatusCode, string(errBody))
}
var result struct {
ID string `json:"id"`
}
json.NewDecoder(resp.Body).Decode(&result)
return result.ID, nil
}
// CheckStatus retrieves the current status of an Onfido check.
func (o *Onfido) CheckStatus(ctx context.Context, verificationID string) (*VerificationStatusResult, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
o.cfg.BaseURL+"/checks/"+verificationID, nil)
if err != nil {
return nil, err
}
o.setHeaders(req)
resp, err := o.client.Do(req)
if err != nil {
return nil, fmt.Errorf("onfido status check failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
errBody, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("onfido status check error %d: %s", resp.StatusCode, string(errBody))
}
var result struct {
ID string `json:"id"`
Status string `json:"status"`
Result string `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("onfido status decode failed: %w", err)
}
status := mapOnfidoResult(result.Result)
checks := []Check{
{Type: "check", Status: result.Result},
}
return &VerificationStatusResult{
VerificationID: verificationID,
Provider: ProviderOnfido,
Status: status,
Checks: checks,
}, nil
}
// ParseWebhook parses an Onfido webhook notification.
func (o *Onfido) ParseWebhook(body []byte, headers map[string]string) (*WebhookEvent, error) {
var payload struct {
Payload struct {
ResourceType string `json:"resource_type"`
Action string `json:"action"`
Object struct {
ID string `json:"id"`
Status string `json:"status"`
Result string `json:"result"`
ApplicantID string `json:"applicant_id"`
} `json:"object"`
} `json:"payload"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, fmt.Errorf("onfido webhook decode: %w", err)
}
status := mapOnfidoResult(payload.Payload.Object.Result)
checks := []Check{
{
Type: payload.Payload.ResourceType,
Status: payload.Payload.Object.Result,
},
}
return &WebhookEvent{
Provider: ProviderOnfido,
VerificationID: payload.Payload.Object.ID,
Status: status,
Checks: checks,
RawPayload: body,
ReceivedAt: time.Now(),
}, nil
}
func (o *Onfido) setHeaders(req *http.Request) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Token token="+o.cfg.APIToken)
}
func mapOnfidoResult(result string) VerificationStatus {
switch result {
case "clear":
return StatusApproved
case "consider":
return StatusDeclined
default:
return StatusPending
}
}