captcha.go
package captcha
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
const api = "https://captcha.example.com"
var client = &http.Client{Timeout: 5 * time.Second}
func randomHex() string {
var b [16]byte
_, _ = rand.Read(b[:])
return hex.EncodeToString(b[:])
}
func post(ctx context.Context, path, idemKey string, body, out any) (int, error) {
raw, err := json.Marshal(body)
if err != nil {
return 0, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, api+path, bytes.NewReader(raw))
if err != nil {
return 0, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("DIGICAPTCHA_CREDENTIAL"))
req.Header.Set("Idempotency-Key", idemKey)
req.Header.Set("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
return 0, err
}
defer res.Body.Close()
return res.StatusCode, json.NewDecoder(res.Body).Decode(out)
}
// RequestHash relies on encoding/json sorting map keys.
func RequestHash(fields map[string]string) string {
raw, _ := json.Marshal(fields)
sum := sha256.Sum256(raw)
return "sha256:" + hex.EncodeToString(sum[:])
}
// CreateIntent returns the intent ID and the hash to keep in the server session.
func CreateIntent(ctx context.Context, action string) (string, string, error) {
hash := RequestHash(map[string]string{"action": action, "nonce": randomHex()})
var out struct {
IntentID string `json:"intent_id"`
}
status, err := post(ctx, "/v1/intents", "intent-"+randomHex(), map[string]any{
"site_id": os.Getenv("DIGICAPTCHA_SITE_ID"),
"action": action,
"request_hash": hash,
"binding_version": "jcs-v1",
"expires_in_seconds": 300,
}, &out)
if err != nil || status != http.StatusCreated {
return "", "", fmt.Errorf("digicaptcha intent: status %d: %v", status, err)
}
return out.IntentID, hash, nil
}
// Verify is true only for HTTP 200 with success:true.
func Verify(ctx context.Context, receipt, intentID, action, hash string) bool {
var out struct {
Success bool `json:"success"`
}
status, err := post(ctx, "/v1/siteverify", "verify-"+intentID, map[string]string{
"receipt": receipt,
"intent_id": intentID,
"action": action,
"request_hash": hash,
}, &out)
return err == nil && status == http.StatusOK && out.Success
}