Skip to content

Commit

Permalink
feat: add minimum password length and password requirements config (#…
Browse files Browse the repository at this point in the history
…2885)

* feat: add minimal password length and password requirements config

* Update auth config body

* fix: change password requirements to a map

* tests: update config and diff files

* update diffs for tests

* chore: convert between local and remote password

* chore: refactor config fields

* chore: add unit tests for password requirement

* chore: add missing testdata

---------

Co-authored-by: Qiao Han <[email protected]>
  • Loading branch information
silentworks and sweatybridge authored Nov 28, 2024
1 parent 8fb7f30 commit b9e0aa0
Show file tree
Hide file tree
Showing 15 changed files with 189 additions and 22 deletions.
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 @@ -112,6 +112,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

0 comments on commit b9e0aa0

Please sign in to comment.