Skip to content

Commit

Permalink
feat: basic hcaptcha support
Browse files Browse the repository at this point in the history
  • Loading branch information
darora committed Aug 29, 2021
1 parent 9b955cb commit b5685fb
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 4 deletions.
8 changes: 4 additions & 4 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati

r.With(api.requireAdminCredentials).Post("/invite", api.Invite)

r.Post("/signup", api.Signup)
r.With(api.requireEmailProvider).Post("/recover", api.Recover)
r.Post("/magiclink", api.MagicLink)
r.Post("/otp", api.Otp)
r.With(api.verifyCaptcha).Post("/signup", api.Signup)
r.With(api.verifyCaptcha).With(api.requireEmailProvider).Post("/recover", api.Recover)
r.With(api.verifyCaptcha).Post("/magiclink", api.MagicLink)
r.With(api.verifyCaptcha).Post("/otp", api.Otp)

r.With(api.requireEmailProvider).With(api.limitHandler(
// Allow requests at a rate of 30 per 5 minutes.
Expand Down
33 changes: 33 additions & 0 deletions api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import (
"bytes"
"context"
"encoding/json"
"github.com/netlify/gotrue/security"
"github.com/sirupsen/logrus"
"io"
"io/ioutil"
"net/http"
"strings"

"github.com/didip/tollbooth/v5"
"github.com/didip/tollbooth/v5/limiter"
Expand Down Expand Up @@ -176,3 +179,33 @@ func (a *API) requireEmailProvider(w http.ResponseWriter, req *http.Request) (co

return ctx, nil
}

func (a *API) verifyCaptcha(w http.ResponseWriter, req *http.Request) (context.Context, error) {
ctx := req.Context()
config := a.getConfig(ctx)
if !config.Security.Captcha.Enabled {
return ctx, nil
}
if config.Security.Captcha.Provider != "hcaptcha" {
logrus.WithField("provider", config.Security.Captcha.Provider).Warn("Unsupported captcha provider")
return nil, internalServerError("server misconfigured")
}
secret := strings.TrimSpace(config.Security.Captcha.Secret)
if secret == "" {
return nil, internalServerError("server misconfigured")
}
verificationResult, err := security.VerifyRequest(req, secret)
if err != nil {
logrus.WithField("err", err).Infof("failed to validate result")
return nil, internalServerError("request validation failure")
}
if verificationResult == security.VerificationProcessFailure {
return nil, internalServerError("request validation failure")
} else if verificationResult == security.UserRequestFailed {
return nil, badRequestError("request disallowed")
}
if verificationResult == security.SuccessfullyVerified {
return ctx, nil
}
return nil, internalServerError("")
}
11 changes: 11 additions & 0 deletions conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ type TwilioProviderConfiguration struct {
MessageServiceSid string `json:"message_service_sid" split_words:"true"`
}

type CaptchaConfiguration struct {
Enabled bool `json:"enabled" default:"false"`
Provider string `json:"provider" default:"hcaptcha"`
Secret string `json:"provider_secret"`
}

type SecurityConfiguration struct {
Captcha CaptchaConfiguration `json:"captcha"`
}

// Configuration holds all the per-instance configuration.
type Configuration struct {
SiteURL string `json:"site_url" split_words:"true" required:"true"`
Expand All @@ -144,6 +154,7 @@ type Configuration struct {
Sms SmsProviderConfiguration `json:"sms"`
DisableSignup bool `json:"disable_signup" split_words:"true"`
Webhook WebhookConfig `json:"webhook" split_words:"true"`
Security SecurityConfiguration `json:"security"`
Cookie struct {
Key string `json:"key"`
Duration int `json:"duration"`
Expand Down
81 changes: 81 additions & 0 deletions security/hcaptcha.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package security

import (
"bytes"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)

type CaptchaResponse struct {
Token string `json:"hcaptchaToken"`
}

type VerificationResponse struct {
Success bool `json:"success"`
ErrorCodes []string `json:"error-codes"`
Hostname string `json:"hostname"`
}

type VerificationResult int
const (
UserRequestFailed VerificationResult = iota
VerificationProcessFailure
SuccessfullyVerified
)

func VerifyRequest(r *http.Request, secretKey string) (VerificationResult, error) {
res := CaptchaResponse{}
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
return UserRequestFailed, err
}
r.Body.Close()
// re-init body so downstream route handlers don't get borked
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

jsonDecoder := json.NewDecoder(bytes.NewBuffer(bodyBytes))
err = jsonDecoder.Decode(&res)
if err != nil || strings.TrimSpace(res.Token) == "" {
return UserRequestFailed, errors.Wrap(err, "couldn't decode captcha info")
}
return verifyCaptchaCode(res.Token, secretKey)
}

func verifyCaptchaCode(token string, secretKey string) (VerificationResult, error) {
data := url.Values{}
data.Set("secret", secretKey)
data.Set("response", token)
// TODO (darora): pipe through sitekey

// TODO (darora): make timeout configurable
client := &http.Client{Timeout: 10 * time.Second}
r, err := http.NewRequest("POST", "https://hcaptcha.com/siteverify", strings.NewReader(data.Encode()))
if err != nil {
return VerificationProcessFailure, errors.Wrap(err, "couldn't initialize request object for hcaptcha check")
}
r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
res, err := client.Do(r)
if err != nil {
return VerificationProcessFailure, errors.Wrap(err, "failed to verify hcaptcha token")
}
verResult := VerificationResponse{}
decoder := json.NewDecoder(res.Body)
err = decoder.Decode(&verResult)
if err != nil {
return VerificationProcessFailure, errors.Wrap(err, "failed to decode hcaptcha response")
}
logrus.WithField("result", verResult).Info("obtained hcaptcha verification result")
if !verResult.Success {
return UserRequestFailed, fmt.Errorf("user request suppressed by hcaptcha")
}
return SuccessfullyVerified, nil
}

0 comments on commit b5685fb

Please sign in to comment.