107 lines
2.7 KiB
Go
107 lines
2.7 KiB
Go
|
|
package auth
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/google/uuid"
|
||
|
|
)
|
||
|
|
|
||
|
|
const defaultTurnstileSiteVerifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
||
|
|
|
||
|
|
type TurnstileVerifierOptions struct {
|
||
|
|
SecretKey string
|
||
|
|
SiteVerifyURL string
|
||
|
|
Timeout time.Duration
|
||
|
|
HTTPClient *http.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
type TurnstileVerifier struct {
|
||
|
|
secretKey string
|
||
|
|
siteVerifyURL string
|
||
|
|
timeout time.Duration
|
||
|
|
client *http.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
type turnstileVerifyRequest struct {
|
||
|
|
Secret string `json:"secret"`
|
||
|
|
Response string `json:"response"`
|
||
|
|
IdempotencyKey string `json:"idempotency_key,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type turnstileVerifyResponse struct {
|
||
|
|
Success bool `json:"success"`
|
||
|
|
ErrorCodes []string `json:"error-codes"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewTurnstileVerifier(opts TurnstileVerifierOptions) (*TurnstileVerifier, error) {
|
||
|
|
secretKey := strings.TrimSpace(opts.SecretKey)
|
||
|
|
if secretKey == "" {
|
||
|
|
return nil, ErrProviderNotReady
|
||
|
|
}
|
||
|
|
siteVerifyURL := strings.TrimSpace(opts.SiteVerifyURL)
|
||
|
|
if siteVerifyURL == "" {
|
||
|
|
siteVerifyURL = defaultTurnstileSiteVerifyURL
|
||
|
|
}
|
||
|
|
timeout := opts.Timeout
|
||
|
|
if timeout <= 0 {
|
||
|
|
timeout = 5 * time.Second
|
||
|
|
}
|
||
|
|
client := opts.HTTPClient
|
||
|
|
if client == nil {
|
||
|
|
client = &http.Client{Timeout: timeout}
|
||
|
|
}
|
||
|
|
return &TurnstileVerifier{
|
||
|
|
secretKey: secretKey,
|
||
|
|
siteVerifyURL: siteVerifyURL,
|
||
|
|
timeout: timeout,
|
||
|
|
client: client,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (v *TurnstileVerifier) Verify(ctx context.Context, token string) error {
|
||
|
|
token = strings.TrimSpace(token)
|
||
|
|
if token == "" {
|
||
|
|
return ErrHumanVerificationRequired
|
||
|
|
}
|
||
|
|
ctx, cancel := context.WithTimeout(ctx, v.timeout)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
body, err := json.Marshal(turnstileVerifyRequest{
|
||
|
|
Secret: v.secretKey,
|
||
|
|
Response: token,
|
||
|
|
IdempotencyKey: uuid.NewString(),
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, v.siteVerifyURL, bytes.NewReader(body))
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
|
||
|
|
resp, err := v.client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("%w: turnstile verification request failed", ErrHumanVerificationInvalid)
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
var result turnstileVerifyResponse
|
||
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
|
|
return fmt.Errorf("%w: turnstile verification response is invalid", ErrHumanVerificationInvalid)
|
||
|
|
}
|
||
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices || !result.Success {
|
||
|
|
if len(result.ErrorCodes) > 0 {
|
||
|
|
return fmt.Errorf("%w: %s", ErrHumanVerificationInvalid, strings.Join(result.ErrorCodes, ","))
|
||
|
|
}
|
||
|
|
return ErrHumanVerificationInvalid
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|