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 HTTP hook Secret Configuration #2129

Merged
merged 3 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions internal/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}

Expand All @@ -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,
)
}

Expand All @@ -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,
)
}

Expand Down
61 changes: 45 additions & 16 deletions internal/utils/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
_ "embed"
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 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 errors.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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
56 changes: 56 additions & 0 deletions internal/utils/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -155,3 +156,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)
}
})
}
}
5 changes: 5 additions & 0 deletions internal/utils/templates/init_config.test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down