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: add minimum password length and password requirements config #2885

2 changes: 2 additions & 0 deletions internal/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,8 @@ EOF
fmt.Sprintf("GOTRUE_SMS_TEMPLATE=%v", utils.Config.Auth.Sms.Template),
"GOTRUE_SMS_TEST_OTP=" + testOTP.String(),

fmt.Sprintf("GOTRUE_PASSWORD_MIN_LENGTH=%v", utils.Config.Auth.MinimumPasswordLength),
fmt.Sprintf("GOTRUE_PASSWORD_REQUIRED_CHARACTERS=%v", utils.Config.Auth.PasswordRequirements.ToChar()),
fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=%v", utils.Config.Auth.EnableRefreshTokenRotation),
fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=%v", utils.Config.Auth.RefreshTokenReuseInterval),
fmt.Sprintf("GOTRUE_SECURITY_MANUAL_LINKING_ENABLED=%v", utils.Config.Auth.EnableManualLinking),
Expand Down
56 changes: 48 additions & 8 deletions pkg/config/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,54 @@ import (
"github.com/supabase/cli/pkg/diff"
)

type PasswordRequirements string

const (
NoRequirements PasswordRequirements = ""
LettersDigits PasswordRequirements = "letters_digits"
LowerUpperLettersDigits PasswordRequirements = "lower_upper_letters_digits"
LowerUpperLettersDigitsSymbols PasswordRequirements = "lower_upper_letters_digits_symbols"
)

func (r PasswordRequirements) ToChar() v1API.UpdateAuthConfigBodyPasswordRequiredCharacters {
switch r {
case LettersDigits:
return v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
case LowerUpperLettersDigits:
return v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891
case LowerUpperLettersDigitsSymbols:
return v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892
}
return v1API.Empty
}

func NewPasswordRequirement(c v1API.UpdateAuthConfigBodyPasswordRequiredCharacters) PasswordRequirements {
switch c {
case v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:
return LettersDigits
case v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567891:
return LowerUpperLettersDigits
case v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567892:
return LowerUpperLettersDigitsSymbols
}
return NoRequirements
}

type (
auth struct {
Enabled bool `toml:"enabled"`
Image string `toml:"-"`

SiteUrl string `toml:"site_url"`
AdditionalRedirectUrls []string `toml:"additional_redirect_urls"`
JwtExpiry uint `toml:"jwt_expiry"`
EnableRefreshTokenRotation bool `toml:"enable_refresh_token_rotation"`
RefreshTokenReuseInterval uint `toml:"refresh_token_reuse_interval"`
EnableManualLinking bool `toml:"enable_manual_linking"`
EnableSignup bool `toml:"enable_signup"`
EnableAnonymousSignIns bool `toml:"enable_anonymous_sign_ins"`
SiteUrl string `toml:"site_url"`
AdditionalRedirectUrls []string `toml:"additional_redirect_urls"`
JwtExpiry uint `toml:"jwt_expiry"`
EnableRefreshTokenRotation bool `toml:"enable_refresh_token_rotation"`
RefreshTokenReuseInterval uint `toml:"refresh_token_reuse_interval"`
EnableManualLinking bool `toml:"enable_manual_linking"`
EnableSignup bool `toml:"enable_signup"`
EnableAnonymousSignIns bool `toml:"enable_anonymous_sign_ins"`
MinimumPasswordLength uint `toml:"minimum_password_length"`
PasswordRequirements PasswordRequirements `toml:"password_requirements"`

Hook hook `toml:"hook"`
MFA mfa `toml:"mfa"`
Expand Down Expand Up @@ -192,6 +227,8 @@ func (a *auth) ToUpdateAuthConfigBody() v1API.UpdateAuthConfigBody {
SecurityManualLinkingEnabled: &a.EnableManualLinking,
DisableSignup: cast.Ptr(!a.EnableSignup),
ExternalAnonymousUsersEnabled: &a.EnableAnonymousSignIns,
PasswordMinLength: cast.UintToIntPtr(&a.MinimumPasswordLength),
PasswordRequiredCharacters: cast.Ptr(a.PasswordRequirements.ToChar()),
}
a.Hook.toAuthConfigBody(&body)
a.MFA.toAuthConfigBody(&body)
Expand All @@ -211,6 +248,9 @@ func (a *auth) FromRemoteAuthConfig(remoteConfig v1API.AuthConfigResponse) {
a.EnableManualLinking = cast.Val(remoteConfig.SecurityManualLinkingEnabled, false)
a.EnableSignup = !cast.Val(remoteConfig.DisableSignup, false)
a.EnableAnonymousSignIns = cast.Val(remoteConfig.ExternalAnonymousUsersEnabled, false)
a.MinimumPasswordLength = cast.IntToUint(cast.Val(remoteConfig.PasswordMinLength, 0))
prc := cast.Val(remoteConfig.PasswordRequiredCharacters, "")
a.PasswordRequirements = NewPasswordRequirement(v1API.UpdateAuthConfigBodyPasswordRequiredCharacters(prc))
a.Hook.fromAuthConfig(remoteConfig)
a.MFA.fromAuthConfig(remoteConfig)
a.Sessions.fromAuthConfig(remoteConfig)
Expand Down
83 changes: 83 additions & 0 deletions pkg/config/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,89 @@ func assertSnapshotEqual(t *testing.T, actual []byte) {
assert.Equal(t, string(expected), string(actual))
}

func TestAuthDiff(t *testing.T) {
t.Run("local and remote enabled", func(t *testing.T) {
c := newWithDefaults()
c.SiteUrl = "http://127.0.0.1:3000"
c.AdditionalRedirectUrls = []string{"https://127.0.0.1:3000"}
c.JwtExpiry = 3600
c.EnableRefreshTokenRotation = true
c.RefreshTokenReuseInterval = 10
c.EnableManualLinking = true
c.EnableSignup = true
c.EnableAnonymousSignIns = true
c.MinimumPasswordLength = 6
c.PasswordRequirements = LettersDigits
// Run test
diff, err := c.DiffWithRemote("", v1API.AuthConfigResponse{
SiteUrl: cast.Ptr("http://127.0.0.1:3000"),
UriAllowList: cast.Ptr("https://127.0.0.1:3000"),
JwtExp: cast.Ptr(3600),
RefreshTokenRotationEnabled: cast.Ptr(true),
SecurityRefreshTokenReuseInterval: cast.Ptr(10),
SecurityManualLinkingEnabled: cast.Ptr(true),
DisableSignup: cast.Ptr(false),
ExternalAnonymousUsersEnabled: cast.Ptr(true),
PasswordMinLength: cast.Ptr(6),
PasswordRequiredCharacters: cast.Ptr(string(v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789)),
})
// Check error
assert.NoError(t, err)
assert.Empty(t, string(diff))
})

t.Run("local enabled and disabled", func(t *testing.T) {
c := newWithDefaults()
c.SiteUrl = "http://127.0.0.1:3000"
c.AdditionalRedirectUrls = []string{"https://127.0.0.1:3000"}
c.JwtExpiry = 3600
c.EnableRefreshTokenRotation = false
c.RefreshTokenReuseInterval = 10
c.EnableManualLinking = false
c.EnableSignup = false
c.EnableAnonymousSignIns = false
c.MinimumPasswordLength = 6
c.PasswordRequirements = LowerUpperLettersDigitsSymbols
// Run test
diff, err := c.DiffWithRemote("", v1API.AuthConfigResponse{
SiteUrl: cast.Ptr(""),
UriAllowList: cast.Ptr("https://127.0.0.1:3000,https://ref.supabase.co"),
JwtExp: cast.Ptr(0),
RefreshTokenRotationEnabled: cast.Ptr(true),
SecurityRefreshTokenReuseInterval: cast.Ptr(0),
SecurityManualLinkingEnabled: cast.Ptr(true),
DisableSignup: cast.Ptr(false),
ExternalAnonymousUsersEnabled: cast.Ptr(true),
PasswordMinLength: cast.Ptr(8),
PasswordRequiredCharacters: cast.Ptr(string(v1API.AbcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789)),
})
// Check error
assert.NoError(t, err)
assertSnapshotEqual(t, diff)
})

t.Run("local and remote disabled", func(t *testing.T) {
c := newWithDefaults()
c.EnableSignup = false
// Run test
diff, err := c.DiffWithRemote("", v1API.AuthConfigResponse{
SiteUrl: cast.Ptr(""),
UriAllowList: cast.Ptr(""),
JwtExp: cast.Ptr(0),
RefreshTokenRotationEnabled: cast.Ptr(false),
SecurityRefreshTokenReuseInterval: cast.Ptr(0),
SecurityManualLinkingEnabled: cast.Ptr(false),
DisableSignup: cast.Ptr(true),
ExternalAnonymousUsersEnabled: cast.Ptr(false),
PasswordMinLength: cast.Ptr(0),
PasswordRequiredCharacters: cast.Ptr(""),
})
// Check error
assert.NoError(t, err)
assert.Empty(t, string(diff))
})
}

func TestHookDiff(t *testing.T) {
t.Run("local and remote enabled", func(t *testing.T) {
c := newWithDefaults()
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,10 @@ func (c *baseConfig) Validate(fsys fs.FS) error {
return errors.Errorf("Invalid config for auth.additional_redirect_urls[%d]: %v", i, err)
}
}
allowed := []PasswordRequirements{NoRequirements, LettersDigits, LowerUpperLettersDigits, LowerUpperLettersDigitsSymbols}
if !sliceContains(allowed, c.Auth.PasswordRequirements) {
return errors.Errorf("Invalid config for auth.password_requirements. Must be one of: %v", allowed)
}
if err := c.Auth.Hook.validate(); err != nil {
return err
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/config/templates/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ enable_signup = true
enable_anonymous_sign_ins = false
# Allow/disallow testing manual linking of accounts
enable_manual_linking = false
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
minimum_password_length = 6
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
password_requirements = ""

[auth.email]
# Allow/disallow new user signups via email to your project.
Expand Down
28 changes: 28 additions & 0 deletions pkg/config/testdata/TestAuthDiff/local_enabled_and_disabled.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
diff remote[auth] local[auth]
--- remote[auth]
+++ local[auth]
@@ -1,14 +1,14 @@
enabled = false
-site_url = ""
-additional_redirect_urls = ["https://127.0.0.1:3000", "https://ref.supabase.co"]
-jwt_expiry = 0
-enable_refresh_token_rotation = true
-refresh_token_reuse_interval = 0
-enable_manual_linking = true
-enable_signup = true
-enable_anonymous_sign_ins = true
-minimum_password_length = 8
-password_requirements = "letters_digits"
+site_url = "http://127.0.0.1:3000"
+additional_redirect_urls = ["https://127.0.0.1:3000"]
+jwt_expiry = 3600
+enable_refresh_token_rotation = false
+refresh_token_reuse_interval = 10
+enable_manual_linking = false
+enable_signup = false
+enable_anonymous_sign_ins = false
+minimum_password_length = 6
+password_requirements = "lower_upper_letters_digits_symbols"

[hook]
[hook.mfa_verification_attempt]
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
diff remote[auth] local[auth]
--- remote[auth]
+++ local[auth]
@@ -49,13 +49,13 @@
@@ -51,13 +51,13 @@
inactivity_timeout = "0s"

[email]
Expand All @@ -22,7 +22,7 @@ diff remote[auth] local[auth]
[email.template]
[email.template.confirmation]
content_path = ""
@@ -69,13 +69,6 @@
@@ -71,13 +71,6 @@
content_path = ""
[email.template.recovery]
content_path = ""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
diff remote[auth] local[auth]
--- remote[auth]
+++ local[auth]
@@ -49,28 +49,43 @@
@@ -51,28 +51,43 @@
inactivity_timeout = "0s"

[email]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
diff remote[auth] local[auth]
--- remote[auth]
+++ local[auth]
@@ -89,7 +89,7 @@
@@ -91,7 +91,7 @@

[external]
[external.apple]
Expand All @@ -10,7 +10,7 @@ diff remote[auth] local[auth]
client_id = "test-client-1,test-client-2"
secret = "hash:ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"
url = ""
@@ -145,7 +145,7 @@
@@ -147,7 +147,7 @@
redirect_uri = ""
skip_nonce_check = false
[external.google]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
diff remote[auth] local[auth]
--- remote[auth]
+++ local[auth]
@@ -9,7 +9,7 @@
@@ -11,7 +11,7 @@

[hook]
[hook.mfa_verification_attempt]
Expand All @@ -10,7 +10,7 @@ diff remote[auth] local[auth]
uri = ""
secrets = ""
[hook.password_verification_attempt]
@@ -17,7 +17,7 @@
@@ -19,7 +19,7 @@
uri = ""
secrets = ""
[hook.custom_access_token]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
diff remote[auth] local[auth]
--- remote[auth]
+++ local[auth]
@@ -30,16 +30,16 @@
@@ -32,16 +32,16 @@
secrets = ""

[mfa]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
diff remote[auth] local[auth]
--- remote[auth]
+++ local[auth]
@@ -58,7 +58,7 @@
@@ -60,7 +60,7 @@
otp_expiry = 0

[sms]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
diff remote[auth] local[auth]
--- remote[auth]
+++ local[auth]
@@ -58,12 +58,12 @@
@@ -60,12 +60,12 @@
otp_expiry = 0

[sms]
Expand All @@ -19,7 +19,7 @@ diff remote[auth] local[auth]
account_sid = ""
message_service_sid = ""
auth_token = ""
@@ -86,8 +86,6 @@
@@ -88,8 +88,6 @@
api_key = ""
api_secret = ""
[sms.test_otp]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
diff remote[auth] local[auth]
--- remote[auth]
+++ local[auth]
@@ -58,12 +58,12 @@
@@ -60,12 +60,12 @@
otp_expiry = 0

[sms]
Expand All @@ -19,7 +19,7 @@ diff remote[auth] local[auth]
account_sid = ""
message_service_sid = ""
auth_token = ""
@@ -73,9 +73,9 @@
@@ -75,9 +75,9 @@
message_service_sid = ""
auth_token = ""
[sms.messagebird]
Expand All @@ -32,7 +32,7 @@ diff remote[auth] local[auth]
[sms.textlocal]
enabled = false
sender = ""
@@ -86,6 +86,7 @@
@@ -88,6 +88,7 @@
api_key = ""
api_secret = ""
[sms.test_otp]
Expand Down
5 changes: 5 additions & 0 deletions pkg/config/testdata/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ refresh_token_reuse_interval = 10
enable_signup = true
# Allow/disallow testing manual linking of accounts
enable_manual_linking = true
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
minimum_password_length = 6
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
password_requirements = ""

[auth.email]
# Allow/disallow new user signups via email to your project.
Expand Down