Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): add rate limit configuration for emails in toml #3206

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
9 changes: 9 additions & 0 deletions internal/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/supabase/cli/internal/status"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/cast"
"github.com/supabase/cli/pkg/config"
"golang.org/x/mod/semver"
)
Expand Down Expand Up @@ -504,6 +505,14 @@ EOF
fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.EnrollEnabled),
fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.VerifyEnabled),
fmt.Sprintf("GOTRUE_MFA_MAX_ENROLLED_FACTORS=%v", utils.Config.Auth.MFA.MaxEnrolledFactors),

// Add rate limit configurations
fmt.Sprintf("GOTRUE_RATE_LIMIT_ANONYMOUS_USERS=%v", cast.Val(utils.Config.Auth.RateLimit.AnonymousUsers, 30)),
fmt.Sprintf("GOTRUE_RATE_LIMIT_TOKEN_REFRESH=%v", cast.Val(utils.Config.Auth.RateLimit.TokenRefresh, 150)),
fmt.Sprintf("GOTRUE_RATE_LIMIT_OTP=%v", cast.Val(utils.Config.Auth.RateLimit.SignInSignUps, 30)),
fmt.Sprintf("GOTRUE_RATE_LIMIT_VERIFY=%v", cast.Val(utils.Config.Auth.RateLimit.TokenVerifications, 30)),
fmt.Sprintf("GOTRUE_RATE_LIMIT_SMS_SENT=%v", cast.Val(utils.Config.Auth.RateLimit.SmsSent, 30)),
fmt.Sprintf("GOTRUE_RATE_LIMIT_EMAIL_SENT=%v", cast.Val(utils.Config.Auth.RateLimit.EmailSent, 2)),
}

if utils.Config.Auth.Email.Smtp != nil && utils.Config.Auth.Email.Smtp.Enabled {
Expand Down
81 changes: 74 additions & 7 deletions pkg/config/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@ type (
MinimumPasswordLength uint `toml:"minimum_password_length"`
PasswordRequirements PasswordRequirements `toml:"password_requirements"`

Captcha *captcha `toml:"captcha"`
Hook hook `toml:"hook"`
MFA mfa `toml:"mfa"`
Sessions sessions `toml:"sessions"`
Email email `toml:"email"`
Sms sms `toml:"sms"`
External external `toml:"external"`
RateLimit rateLimit `toml:"rate_limit"`
Captcha *captcha `toml:"captcha"`
Hook hook `toml:"hook"`
MFA mfa `toml:"mfa"`
Sessions sessions `toml:"sessions"`
Email email `toml:"email"`
Sms sms `toml:"sms"`
External external `toml:"external"`

// Custom secrets can be injected from .env file
JwtSecret string `toml:"-" mapstructure:"jwt_secret"`
Expand All @@ -107,6 +108,15 @@ type (
Cognito tpaCognito `toml:"aws_cognito"`
}

rateLimit struct {
AnonymousUsers *uint `toml:"anonymous_users"`
TokenRefresh *uint `toml:"token_refresh"`
SignInSignUps *uint `toml:"sign_in_sign_ups"`
TokenVerifications *uint `toml:"token_verifications"`
EmailSent *uint `toml:"email_sent"`
SmsSent *uint `toml:"sms_sent"`
Copy link
Contributor

@sweatybridge sweatybridge Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any objection to make these non-pointers? We are setting default values for these fields https://github.com/supabase/cli/pull/3206/files#diff-ec116054b1a1deee8fc9bfb3c95419fcdae3c4d5a2c0b7a25c7c55ac1cfb135dR130-R142 so only the commented out EmailSent and SmsSent are nil pointers.

}

tpaFirebase struct {
Enabled bool `toml:"enabled"`

Expand Down Expand Up @@ -249,6 +259,54 @@ type (
}
)

func (r rateLimit) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {
if r.AnonymousUsers != nil {
body.RateLimitAnonymousUsers = cast.UintToIntPtr(r.AnonymousUsers)
}
if r.TokenRefresh != nil {
body.RateLimitTokenRefresh = cast.UintToIntPtr(r.TokenRefresh)
}
if r.SignInSignUps != nil {
body.RateLimitOtp = cast.UintToIntPtr(r.SignInSignUps)
}
if r.TokenVerifications != nil {
body.RateLimitVerify = cast.UintToIntPtr(r.TokenVerifications)
}
if r.EmailSent != nil {
body.RateLimitEmailSent = cast.UintToIntPtr(r.EmailSent)
}
if r.SmsSent != nil {
body.RateLimitSmsSent = cast.UintToIntPtr(r.SmsSent)
}
}

func (r *rateLimit) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
if remoteConfig.RateLimitAnonymousUsers != nil {
val := cast.IntToUint(*remoteConfig.RateLimitAnonymousUsers)
r.AnonymousUsers = &val
}
if remoteConfig.RateLimitTokenRefresh != nil {
val := cast.IntToUint(*remoteConfig.RateLimitTokenRefresh)
r.TokenRefresh = &val
}
if remoteConfig.RateLimitOtp != nil {
val := cast.IntToUint(*remoteConfig.RateLimitOtp)
r.SignInSignUps = &val
}
if remoteConfig.RateLimitVerify != nil {
val := cast.IntToUint(*remoteConfig.RateLimitVerify)
r.TokenVerifications = &val
}
if remoteConfig.RateLimitEmailSent != nil {
val := cast.IntToUint(*remoteConfig.RateLimitEmailSent)
r.EmailSent = &val
}
if remoteConfig.RateLimitSmsSent != nil {
val := cast.IntToUint(*remoteConfig.RateLimitSmsSent)
r.SmsSent = &val
}
}

func (a *auth) ToUpdateAuthConfigBody() v1API.UpdateAuthConfigBody {
body := v1API.UpdateAuthConfigBody{
SiteUrl: &a.SiteUrl,
Expand All @@ -262,6 +320,10 @@ func (a *auth) ToUpdateAuthConfigBody() v1API.UpdateAuthConfigBody {
PasswordMinLength: cast.UintToIntPtr(&a.MinimumPasswordLength),
PasswordRequiredCharacters: cast.Ptr(a.PasswordRequirements.ToChar()),
}

// Add rate limit fields
a.RateLimit.toAuthConfigBody(&body)

// When local config is not set, we assume platform defaults should not change
if a.Captcha != nil {
a.Captcha.toAuthConfigBody(&body)
Expand All @@ -287,6 +349,8 @@ func (a *auth) FromRemoteAuthConfig(remoteConfig v1API.AuthConfigResponse) {
a.MinimumPasswordLength = cast.IntToUint(cast.Val(remoteConfig.PasswordMinLength, 0))
prc := cast.Val(remoteConfig.PasswordRequiredCharacters, "")
a.PasswordRequirements = NewPasswordRequirement(v1API.UpdateAuthConfigBodyPasswordRequiredCharacters(prc))

a.RateLimit.fromAuthConfig(remoteConfig)
a.Captcha.fromAuthConfig(remoteConfig)
a.Hook.fromAuthConfig(remoteConfig)
a.MFA.fromAuthConfig(remoteConfig)
Expand Down Expand Up @@ -459,6 +523,7 @@ func (e email) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) {
body.MailerOtpExp = cast.UintToIntPtr(&e.OtpExpiry)
body.SecurityUpdatePasswordRequireReauthentication = &e.SecurePasswordChange
body.SmtpMaxFrequency = cast.Ptr(int(e.MaxFrequency.Seconds()))

// When local config is not set, we assume platform defaults should not change
if e.Smtp != nil {
e.Smtp.toAuthConfigBody(body)
Expand Down Expand Up @@ -495,6 +560,7 @@ func (e *email) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
e.OtpExpiry = cast.IntToUint(remoteConfig.MailerOtpExp)
e.SecurePasswordChange = cast.Val(remoteConfig.SecurityUpdatePasswordRequireReauthentication, false)
e.MaxFrequency = time.Duration(cast.Val(remoteConfig.SmtpMaxFrequency, 0)) * time.Second

e.Smtp.fromAuthConfig(remoteConfig)
if len(e.Template) == 0 {
return
Expand Down Expand Up @@ -648,6 +714,7 @@ func (s *sms) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) {
s.EnableConfirmations = cast.Val(remoteConfig.SmsAutoconfirm, false)
s.Template = cast.Val(remoteConfig.SmsTemplate, "")
s.TestOTP = envToMap(cast.Val(remoteConfig.SmsTestOtp, ""))

// We are only interested in the provider that's enabled locally
switch {
case s.Twilio.Enabled:
Expand Down
Loading