From a6fc3d40306018a5dfa3654ab929b9e5a36ab5b6 Mon Sep 17 00:00:00 2001 From: joel Date: Sat, 6 Apr 2024 11:07:37 +0800 Subject: [PATCH 1/3] feat: add hook secrets --- internal/start/start.go | 21 ++++++++++++ internal/utils/config.go | 61 ++++++++++++++++++++++++++--------- internal/utils/config_test.go | 55 +++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 16 deletions(-) diff --git a/internal/start/start.go b/internal/start/start.go index ad95e7995..331261196 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -497,6 +497,7 @@ EOF env, "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED=true", "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI="+utils.Config.Auth.Hook.MFAVerificationAttempt.URI, + "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_SECRETS="+utils.Config.Auth.Hook.MFAVerificationAttempt.Secrets, ) } @@ -505,6 +506,7 @@ EOF env, "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED=true", "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI="+utils.Config.Auth.Hook.PasswordVerificationAttempt.URI, + "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_SECRETS="+utils.Config.Auth.Hook.PasswordVerificationAttempt.Secrets, ) } @@ -513,6 +515,25 @@ EOF env, "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED=true", "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI="+utils.Config.Auth.Hook.CustomAccessToken.URI, + "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS="+utils.Config.Auth.Hook.CustomAccessToken.Secrets, + ) + } + + if utils.Config.Auth.Hook.SendSMS.Enabled { + env = append( + env, + "GOTRUE_HOOK_SEND_SMS_ENABLED=true", + "GOTRUE_HOOK_SEND_SMS_URI="+utils.Config.Auth.Hook.SendSMS.URI, + "GOTRUE_HOOK_SEND_SMS_SECRETS="+utils.Config.Auth.Hook.SendSMS.Secrets, + ) + } + + if utils.Config.Auth.Hook.SendEmail.Enabled { + env = append( + env, + "GOTRUE_HOOK_SEND_EMAIL_ENABLED=true", + "GOTRUE_HOOK_SEND_EMAIL_URI="+utils.Config.Auth.Hook.SendEmail.URI, + "GOTRUE_HOOK_SEND_EMAIL_SECRETS="+utils.Config.Auth.Hook.SendEmail.Secrets, ) } diff --git a/internal/utils/config.go b/internal/utils/config.go index abb8f3902..02e3f1e2b 100644 --- a/internal/utils/config.go +++ b/internal/utils/config.go @@ -4,6 +4,7 @@ import ( "bytes" _ "embed" "fmt" + "net/url" "os" "path/filepath" "regexp" @@ -387,11 +388,14 @@ type ( MFAVerificationAttempt hookConfig `toml:"mfa_verification_attempt"` PasswordVerificationAttempt hookConfig `toml:"password_verification_attempt"` CustomAccessToken hookConfig `toml:"custom_access_token"` + SendSMS hookConfig `toml:"send_sms"` + SendEmail hookConfig `toml:"send_email"` } hookConfig struct { Enabled bool `toml:"enabled"` URI string `toml:"uri"` + Secrets string `toml:"secrets"` } twilioConfig struct { @@ -460,6 +464,24 @@ type ( // } ) +func (h *hookConfig) HandleHook(hookType string) error { + // If not enabled do nothing + if !h.Enabled { + return nil + } + if h.URI == "" { + return fmt.Errorf("missing required field in config: auth.hook.%s.uri", hookType) + } + if err := validateHookURI(h.URI, hookType); err != nil { + return err + } + var err error + if h.Secrets, err = maybeLoadEnv(h.Secrets); err != nil { + return fmt.Errorf("missing required field in config: auth.hook.%s.secrets", hookType) + } + return nil +} + func LoadConfigFS(fsys afero.Fs) error { // Load default values var buf bytes.Buffer @@ -687,25 +709,21 @@ func LoadConfigFS(fsys afero.Fs) error { return err } } - - if Config.Auth.Hook.MFAVerificationAttempt.Enabled { - if Config.Auth.Hook.MFAVerificationAttempt.URI == "" { - return errors.New("Missing required field in config: auth.hook.mfa_verification_atempt.uri") - } + if err := Config.Auth.Hook.MFAVerificationAttempt.HandleHook("mfa_verification_attempt"); err != nil { + return err } - - if Config.Auth.Hook.PasswordVerificationAttempt.Enabled { - if Config.Auth.Hook.PasswordVerificationAttempt.URI == "" { - return errors.New("Missing required field in config: auth.hook.password_verification_attempt.uri") - } + if err := Config.Auth.Hook.PasswordVerificationAttempt.HandleHook("password_verification_attempt"); err != nil { + return err } - - if Config.Auth.Hook.CustomAccessToken.Enabled { - if Config.Auth.Hook.CustomAccessToken.URI == "" { - return errors.New("Missing required field in config: auth.hook.custom_access_token.uri") - } + if err := Config.Auth.Hook.CustomAccessToken.HandleHook("custom_access_token"); err != nil { + return err + } + if err := Config.Auth.Hook.SendSMS.HandleHook("send_sms"); err != nil { + return err + } + if err := Config.Auth.Hook.SendEmail.HandleHook("send_email"); err != nil { + return err } - // Validate oauth config for ext, provider := range Config.Auth.External { if !provider.Enabled { @@ -862,3 +880,14 @@ func loadEnvIfExists(path string) error { } return nil } + +func validateHookURI(uri, hookName string) error { + parsed, err := url.Parse(uri) + if err != nil { + return errors.Errorf("failed to parse template url: %w", err) + } + if !(parsed.Scheme == "http" || parsed.Scheme == "https" || parsed.Scheme == "pg-functions") { + return errors.Errorf("Invalid HTTP hook config: auth.hook.%v should be a Postgres function URI, or a HTTP or HTTPS URL", hookName) + } + return nil +} diff --git a/internal/utils/config_test.go b/internal/utils/config_test.go index 5a0c5d5a8..06b8873b9 100644 --- a/internal/utils/config_test.go +++ b/internal/utils/config_test.go @@ -155,3 +155,58 @@ func TestSigningJWT(t *testing.T) { assert.Equal(t, defaultServiceRoleKey, signed) }) } + +func TestValidateHookURI(t *testing.T) { + tests := []struct { + name string + uri string + hookName string + shouldErr bool + errorMsg string + }{ + { + name: "valid http URL", + uri: "http://example.com", + hookName: "testHook", + shouldErr: false, + }, + { + name: "valid https URL", + uri: "https://example.com", + hookName: "testHook", + shouldErr: false, + }, + { + name: "valid pg-functions URI", + uri: "pg-functions://functionName", + hookName: "pgHook", + shouldErr: false, + }, + { + name: "invalid URI with unsupported scheme", + uri: "ftp://example.com", + hookName: "malformedHook", + shouldErr: true, + errorMsg: "Invalid HTTP hook config: auth.hook.malformedHook should be a Postgres function URI, or a HTTP or HTTPS URL", + }, + { + name: "invalid URI with parsing error", + uri: "http://a b.com", + hookName: "errorHook", + shouldErr: true, + errorMsg: "failed to parse template url: parse \"http://a b.com\": invalid character \" \" in host name", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateHookURI(tt.uri, tt.hookName) + if tt.shouldErr { + assert.Error(t, err, "Expected an error for %v", tt.name) + assert.EqualError(t, err, tt.errorMsg, "Expected error message does not match for %v", tt.name) + } else { + assert.NoError(t, err, "Expected no error for %v", tt.name) + } + }) + } +} From 64145694f35f9640b2d7419be4fd82109b66c743 Mon Sep 17 00:00:00 2001 From: Han Qiao Date: Wed, 8 May 2024 15:48:32 +0800 Subject: [PATCH 2/3] Apply suggestions from code review --- internal/utils/config.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/utils/config.go b/internal/utils/config.go index 02e3f1e2b..c68a1fb33 100644 --- a/internal/utils/config.go +++ b/internal/utils/config.go @@ -470,14 +470,14 @@ func (h *hookConfig) HandleHook(hookType string) error { return nil } if h.URI == "" { - return fmt.Errorf("missing required field in config: auth.hook.%s.uri", hookType) + return errors.Errorf("missing required field in config: auth.hook.%s.uri", hookType) } if err := validateHookURI(h.URI, hookType); err != nil { return err } var err error if h.Secrets, err = maybeLoadEnv(h.Secrets); err != nil { - return fmt.Errorf("missing required field in config: auth.hook.%s.secrets", hookType) + return errors.Errorf("missing required field in config: auth.hook.%s.secrets", hookType) } return nil } From 63f0f7c8eafcc3eb189ebbc947b43868a65ea690 Mon Sep 17 00:00:00 2001 From: joel Date: Wed, 8 May 2024 15:55:21 +0800 Subject: [PATCH 3/3] fix: add send sms hook as test --- internal/utils/config_test.go | 1 + internal/utils/templates/init_config.test.toml | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/internal/utils/config_test.go b/internal/utils/config_test.go index 06b8873b9..9dae64279 100644 --- a/internal/utils/config_test.go +++ b/internal/utils/config_test.go @@ -41,6 +41,7 @@ func TestConfigParsing(t *testing.T) { t.Setenv("TWILIO_AUTH_TOKEN", "token") t.Setenv("AZURE_CLIENT_ID", "hello") t.Setenv("AZURE_SECRET", "this is cool") + t.Setenv("AUTH_SEND_SMS_SECRETS", "v1,whsec_aWxpa2VzdXBhYmFzZXZlcnltdWNoYW5kaWhvcGV5b3Vkb3Rvbw==") assert.NoError(t, LoadConfigFS(fsys)) // Check error assert.Equal(t, "hello", Config.Auth.External["azure"].ClientId) diff --git a/internal/utils/templates/init_config.test.toml b/internal/utils/templates/init_config.test.toml index f485c5218..aab9265d4 100644 --- a/internal/utils/templates/init_config.test.toml +++ b/internal/utils/templates/init_config.test.toml @@ -123,6 +123,11 @@ max_frequency = "5s" enabled = true uri = "pg-functions://postgres/auth/custom-access-token-hook" +[auth.hook.send_sms] +enabled = true +uri = "http://host.docker.internal/functions/v1/send_sms" +secrets = "env(AUTH_SEND_SMS_SECRETS)" + # Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. [auth.sms.twilio]