Skip to content

Commit

Permalink
fix: add rate limiter for email endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
kangmingtay committed Sep 19, 2021
1 parent d0982b3 commit 97e9ec8
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 8 deletions.
12 changes: 6 additions & 6 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati

r.Get("/authorize", api.ExternalProviderRedirect)

r.With(api.requireAdminCredentials).Post("/invite", api.Invite)
r.With(api.limitEmailSentHandler()).With(api.requireAdminCredentials).Post("/invite", api.Invite)
r.With(api.limitEmailSentHandler()).With(api.verifyCaptcha).Post("/signup", api.Signup)
r.With(api.limitEmailSentHandler()).With(api.verifyCaptcha).With(api.requireEmailProvider).Post("/recover", api.Recover)
r.With(api.limitEmailSentHandler()).With(api.verifyCaptcha).Post("/magiclink", api.MagicLink)

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.limitEmailSentHandler()).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 All @@ -143,7 +143,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati
r.Route("/user", func(r *router) {
r.Use(api.requireAuthentication)
r.Get("/", api.UserGet)
r.Put("/", api.UserUpdate)
r.With(api.limitEmailSentHandler()).Put("/", api.UserUpdate)
})

r.Route("/admin", func(r *router) {
Expand Down
44 changes: 42 additions & 2 deletions api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"bytes"
"context"
"encoding/json"
"github.com/netlify/gotrue/security"
"github.com/sirupsen/logrus"
"io"
"io/ioutil"
"net/http"
"strings"
"time"

"github.com/netlify/gotrue/security"
"github.com/sirupsen/logrus"

"github.com/didip/tollbooth/v5"
"github.com/didip/tollbooth/v5/limiter"
Expand Down Expand Up @@ -154,6 +156,44 @@ func (a *API) limitHandler(lmt *limiter.Limiter) middlewareHandler {
}
}

func (a *API) limitEmailSentHandler() middlewareHandler {
// limit per hour
freq := a.config.RateLimitEmailSent / (60 * 60)
lmt := tollbooth.NewLimiter(freq, &limiter.ExpirableOptions{
DefaultExpirationTTL: time.Hour,
}).SetBurst(int(a.config.RateLimitEmailSent)).SetMethods([]string{"PUT", "POST"})
return func(w http.ResponseWriter, req *http.Request) (context.Context, error) {
c := req.Context()
config := a.getConfig(c)
if config.External.Email.Enabled && !config.Mailer.Autoconfirm {
if req.Method == "PUT" || req.Method == "POST" {
res := make(map[string]interface{})
bodyBytes, err := ioutil.ReadAll(req.Body)
if err != nil {
return c, internalServerError("Error invalid request body").WithInternalError(err)
}
req.Body.Close()
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

jsonDecoder := json.NewDecoder(bytes.NewBuffer(bodyBytes))
if err := jsonDecoder.Decode(&res); err != nil {
return c, badRequestError("Error invalid request body").WithInternalError(err)
}

if _, ok := res["email"]; !ok {
// email not in POST body
return c, nil
}

if err := tollbooth.LimitByRequest(lmt, w, req); err != nil {
return c, httpError(http.StatusTooManyRequests, "Rate limit exceeded")
}
}
}
return c, nil
}
}

func (a *API) requireAdminCredentials(w http.ResponseWriter, req *http.Request) (context.Context, error) {
ctx := req.Context()
t, err := a.extractBearerToken(w, req)
Expand Down

0 comments on commit 97e9ec8

Please sign in to comment.