diff --git a/internal/start/start.go b/internal/start/start.go index 486f2a860..5521161f8 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -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), diff --git a/pkg/config/auth.go b/pkg/config/auth.go index e78092bde..572b88950 100644 --- a/pkg/config/auth.go +++ b/pkg/config/auth.go @@ -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"` @@ -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) @@ -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) diff --git a/pkg/config/auth_test.go b/pkg/config/auth_test.go index 119cdf65d..cba5cd9a1 100644 --- a/pkg/config/auth_test.go +++ b/pkg/config/auth_test.go @@ -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() diff --git a/pkg/config/config.go b/pkg/config/config.go index ff5fdee8c..8d809c595 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 } diff --git a/pkg/config/templates/config.toml b/pkg/config/templates/config.toml index 902330d3c..12decc3f4 100644 --- a/pkg/config/templates/config.toml +++ b/pkg/config/templates/config.toml @@ -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. diff --git a/pkg/config/testdata/TestAuthDiff/local_enabled_and_disabled.diff b/pkg/config/testdata/TestAuthDiff/local_enabled_and_disabled.diff new file mode 100644 index 000000000..3db4d5462 --- /dev/null +++ b/pkg/config/testdata/TestAuthDiff/local_enabled_and_disabled.diff @@ -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] diff --git a/pkg/config/testdata/TestEmailDiff/local_disabled_remote_enabled.diff b/pkg/config/testdata/TestEmailDiff/local_disabled_remote_enabled.diff index d6c7f6dca..3123336fa 100644 --- a/pkg/config/testdata/TestEmailDiff/local_disabled_remote_enabled.diff +++ b/pkg/config/testdata/TestEmailDiff/local_disabled_remote_enabled.diff @@ -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] @@ -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 = "" diff --git a/pkg/config/testdata/TestEmailDiff/local_enabled_remote_disabled.diff b/pkg/config/testdata/TestEmailDiff/local_enabled_remote_disabled.diff index 9bcf8ccba..24386ae91 100644 --- a/pkg/config/testdata/TestEmailDiff/local_enabled_remote_disabled.diff +++ b/pkg/config/testdata/TestEmailDiff/local_enabled_remote_disabled.diff @@ -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] diff --git a/pkg/config/testdata/TestExternalDiff/local_enabled_and_disabled.diff b/pkg/config/testdata/TestExternalDiff/local_enabled_and_disabled.diff index 7be4a13c7..234e422f4 100644 --- a/pkg/config/testdata/TestExternalDiff/local_enabled_and_disabled.diff +++ b/pkg/config/testdata/TestExternalDiff/local_enabled_and_disabled.diff @@ -1,7 +1,7 @@ diff remote[auth] local[auth] --- remote[auth] +++ local[auth] -@@ -89,7 +89,7 @@ +@@ -91,7 +91,7 @@ [external] [external.apple] @@ -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] diff --git a/pkg/config/testdata/TestHookDiff/local_enabled_and_disabled.diff b/pkg/config/testdata/TestHookDiff/local_enabled_and_disabled.diff index b45808d4c..b21cd4073 100644 --- a/pkg/config/testdata/TestHookDiff/local_enabled_and_disabled.diff +++ b/pkg/config/testdata/TestHookDiff/local_enabled_and_disabled.diff @@ -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] @@ -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] diff --git a/pkg/config/testdata/TestMfaDiff/local_enabled_and_disabled.diff b/pkg/config/testdata/TestMfaDiff/local_enabled_and_disabled.diff index f7935bfa4..866ce8fae 100644 --- a/pkg/config/testdata/TestMfaDiff/local_enabled_and_disabled.diff +++ b/pkg/config/testdata/TestMfaDiff/local_enabled_and_disabled.diff @@ -1,7 +1,7 @@ diff remote[auth] local[auth] --- remote[auth] +++ local[auth] -@@ -30,16 +30,16 @@ +@@ -32,16 +32,16 @@ secrets = "" [mfa] diff --git a/pkg/config/testdata/TestSmsDiff/enable_sign_up_without_provider.diff b/pkg/config/testdata/TestSmsDiff/enable_sign_up_without_provider.diff index 2e44496fd..637a0206e 100644 --- a/pkg/config/testdata/TestSmsDiff/enable_sign_up_without_provider.diff +++ b/pkg/config/testdata/TestSmsDiff/enable_sign_up_without_provider.diff @@ -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] diff --git a/pkg/config/testdata/TestSmsDiff/local_disabled_remote_enabled.diff b/pkg/config/testdata/TestSmsDiff/local_disabled_remote_enabled.diff index f9a0428f3..4348c80ba 100644 --- a/pkg/config/testdata/TestSmsDiff/local_disabled_remote_enabled.diff +++ b/pkg/config/testdata/TestSmsDiff/local_disabled_remote_enabled.diff @@ -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] @@ -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] diff --git a/pkg/config/testdata/TestSmsDiff/local_enabled_remote_disabled.diff b/pkg/config/testdata/TestSmsDiff/local_enabled_remote_disabled.diff index 1c0de48f3..e29c287ed 100644 --- a/pkg/config/testdata/TestSmsDiff/local_enabled_remote_disabled.diff +++ b/pkg/config/testdata/TestSmsDiff/local_enabled_remote_disabled.diff @@ -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] @@ -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] @@ -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] diff --git a/pkg/config/testdata/config.toml b/pkg/config/testdata/config.toml index a97042954..ce845743e 100644 --- a/pkg/config/testdata/config.toml +++ b/pkg/config/testdata/config.toml @@ -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.