diff --git a/internal/start/start.go b/internal/start/start.go index 122080c58d..fcf654fe61 100644 --- a/internal/start/start.go +++ b/internal/start/start.go @@ -641,6 +641,14 @@ EOF "GOTRUE_HOOK_SEND_EMAIL_SECRETS="+hook.Secrets.Value, ) } + if hook := utils.Config.Auth.Hook.BeforeUserCreated; hook != nil && hook.Enabled { + env = append( + env, + "GOTRUE_HOOK_BEFORE_USER_CREATED_ENABLED=true", + "GOTRUE_HOOK_BEFORE_USER_CREATED_URI="+hook.URI, + "GOTRUE_HOOK_BEFORE_USER_CREATED_SECRETS="+hook.Secrets.Value, + ) + } if utils.Config.Auth.MFA.Phone.EnrollEnabled || utils.Config.Auth.MFA.Phone.VerifyEnabled { env = append( diff --git a/pkg/config/auth.go b/pkg/config/auth.go index 50194af1bc..9aa0263151 100644 --- a/pkg/config/auth.go +++ b/pkg/config/auth.go @@ -201,6 +201,7 @@ type ( CustomAccessToken *hookConfig `toml:"custom_access_token"` SendSMS *hookConfig `toml:"send_sms"` SendEmail *hookConfig `toml:"send_email"` + BeforeUserCreated *hookConfig `toml:"before_user_created"` } factorTypeConfiguration struct { @@ -381,6 +382,14 @@ func (c *captcha) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) { func (h hook) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { // When local config is not set, we assume platform defaults should not change + if hook := h.BeforeUserCreated; hook != nil { + if body.HookBeforeUserCreatedEnabled = nullable.NewNullableWithValue(hook.Enabled); hook.Enabled { + body.HookBeforeUserCreatedUri = nullable.NewNullableWithValue(hook.URI) + if len(hook.Secrets.SHA256) > 0 { + body.HookBeforeUserCreatedSecrets = nullable.NewNullableWithValue(hook.Secrets.Value) + } + } + } if hook := h.CustomAccessToken; hook != nil { if body.HookCustomAccessTokenEnabled = nullable.NewNullableWithValue(hook.Enabled); hook.Enabled { body.HookCustomAccessTokenUri = nullable.NewNullableWithValue(hook.URI) @@ -425,6 +434,16 @@ func (h hook) toAuthConfigBody(body *v1API.UpdateAuthConfigBody) { } func (h *hook) fromAuthConfig(remoteConfig v1API.AuthConfigResponse) { // When local config is not set, we assume platform defaults should not change + if hook := h.BeforeUserCreated; hook != nil { + // Ignore disabled hooks because their envs are not loaded + if hook.Enabled { + hook.URI = ValOrDefault(remoteConfig.HookBeforeUserCreatedUri, "") + if len(hook.Secrets.SHA256) > 0 { + hook.Secrets.SHA256 = ValOrDefault(remoteConfig.HookBeforeUserCreatedSecrets, "") + } + } + hook.Enabled = ValOrDefault(remoteConfig.HookBeforeUserCreatedEnabled, false) + } if hook := h.CustomAccessToken; hook != nil { // Ignore disabled hooks because their envs are not loaded if hook.Enabled { diff --git a/pkg/config/auth_test.go b/pkg/config/auth_test.go index 1bab298474..bba2123834 100644 --- a/pkg/config/auth_test.go +++ b/pkg/config/auth_test.go @@ -215,6 +215,14 @@ func TestHookDiff(t *testing.T) { t.Run("local and remote enabled", func(t *testing.T) { c := newWithDefaults() c.Hook = hook{ + BeforeUserCreated: &hookConfig{ + Enabled: true, + URI: "http://example.com", + Secrets: Secret{ + Value: "test-secret", + SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", + }, + }, CustomAccessToken: &hookConfig{ Enabled: true, URI: "http://example.com", @@ -254,6 +262,9 @@ func TestHookDiff(t *testing.T) { } // Run test diff, err := c.DiffWithRemote(v1API.AuthConfigResponse{ + HookBeforeUserCreatedEnabled: nullable.NewNullableWithValue(true), + HookBeforeUserCreatedUri: nullable.NewNullableWithValue("http://example.com"), + HookBeforeUserCreatedSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), HookCustomAccessTokenEnabled: nullable.NewNullableWithValue(true), HookCustomAccessTokenUri: nullable.NewNullableWithValue("http://example.com"), HookCustomAccessTokenSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), @@ -277,6 +288,9 @@ func TestHookDiff(t *testing.T) { t.Run("local disabled remote enabled", func(t *testing.T) { c := newWithDefaults() c.Hook = hook{ + BeforeUserCreated: &hookConfig{ + Enabled: false, + }, CustomAccessToken: &hookConfig{ Enabled: false, }, @@ -296,6 +310,9 @@ func TestHookDiff(t *testing.T) { } // Run test diff, err := c.DiffWithRemote(v1API.AuthConfigResponse{ + HookBeforeUserCreatedEnabled: nullable.NewNullableWithValue(true), + HookBeforeUserCreatedUri: nullable.NewNullableWithValue("http://example.com"), + HookBeforeUserCreatedSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), HookCustomAccessTokenEnabled: nullable.NewNullableWithValue(true), HookCustomAccessTokenUri: nullable.NewNullableWithValue("http://example.com"), HookCustomAccessTokenSecrets: nullable.NewNullableWithValue("ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252"), @@ -318,6 +335,14 @@ func TestHookDiff(t *testing.T) { t.Run("local enabled remote disabled", func(t *testing.T) { c := newWithDefaults() c.Hook = hook{ + BeforeUserCreated: &hookConfig{ + Enabled: true, + URI: "http://example.com", + Secrets: Secret{ + Value: "test-secret", + SHA256: "ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252", + }, + }, CustomAccessToken: &hookConfig{ Enabled: true, URI: "http://example.com", @@ -346,6 +371,8 @@ func TestHookDiff(t *testing.T) { } // Run test diff, err := c.DiffWithRemote(v1API.AuthConfigResponse{ + HookBeforeUserCreatedEnabled: nullable.NewNullableWithValue(false), + HookBeforeUserCreatedUri: nullable.NewNullableWithValue("pg-functions://postgres/public/beforeUserCreated"), HookCustomAccessTokenEnabled: nullable.NewNullableWithValue(false), HookCustomAccessTokenUri: nullable.NewNullableWithValue("pg-functions://postgres/public/customToken"), HookSendSmsEnabled: nullable.NewNullableWithValue(false), @@ -366,6 +393,7 @@ func TestHookDiff(t *testing.T) { t.Run("local and remote disabled", func(t *testing.T) { c := newWithDefaults() c.Hook = hook{ + BeforeUserCreated: &hookConfig{Enabled: false}, CustomAccessToken: &hookConfig{Enabled: false}, SendSMS: &hookConfig{Enabled: false}, SendEmail: &hookConfig{Enabled: false}, @@ -374,6 +402,7 @@ func TestHookDiff(t *testing.T) { } // Run test diff, err := c.DiffWithRemote(v1API.AuthConfigResponse{ + HookBeforeUserCreatedEnabled: nullable.NewNullableWithValue(false), HookCustomAccessTokenEnabled: nullable.NewNullableWithValue(false), HookSendSmsEnabled: nullable.NewNullableWithValue(false), HookSendEmailEnabled: nullable.NewNullableWithValue(false), diff --git a/pkg/config/config.go b/pkg/config/config.go index 75bf61fcd0..9f9e05ee92 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -301,6 +301,10 @@ func (a *auth) Clone() auth { hook := *a.Hook.SendEmail copy.Hook.SendEmail = &hook } + if a.Hook.BeforeUserCreated != nil { + hook := *a.Hook.BeforeUserCreated + copy.Hook.BeforeUserCreated = &hook + } copy.Sms.TestOTP = maps.Clone(a.Sms.TestOTP) return copy } @@ -1161,6 +1165,11 @@ func (h *hook) validate() error { return err } } + if hook := h.BeforeUserCreated; hook != nil { + if err := hook.validate("before_user_created"); err != nil { + return err + } + } return nil } diff --git a/pkg/config/templates/config.toml b/pkg/config/templates/config.toml index d72ae50459..92a5367c18 100644 --- a/pkg/config/templates/config.toml +++ b/pkg/config/templates/config.toml @@ -204,6 +204,11 @@ max_frequency = "5s" # Force log out if the user has been inactive longer than the specified duration. # inactivity_timeout = "8h" +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. # [auth.hook.custom_access_token] # enabled = true diff --git a/pkg/config/testdata/TestHookDiff/local_disabled_remote_enabled.diff b/pkg/config/testdata/TestHookDiff/local_disabled_remote_enabled.diff index 459c58f422..43bab9e109 100644 --- a/pkg/config/testdata/TestHookDiff/local_disabled_remote_enabled.diff +++ b/pkg/config/testdata/TestHookDiff/local_disabled_remote_enabled.diff @@ -1,7 +1,7 @@ diff remote[auth] local[auth] --- remote[auth] +++ local[auth] -@@ -24,19 +24,19 @@ +@@ -24,23 +24,23 @@ [hook] [hook.mfa_verification_attempt] @@ -24,4 +24,9 @@ diff remote[auth] local[auth] +enabled = false uri = "" secrets = "" + [hook.before_user_created] +-enabled = true ++enabled = false + uri = "" + secrets = "" diff --git a/pkg/config/testdata/TestHookDiff/local_enabled_remote_disabled.diff b/pkg/config/testdata/TestHookDiff/local_enabled_remote_disabled.diff index 6065f9d203..ed61a4cdb8 100644 --- a/pkg/config/testdata/TestHookDiff/local_enabled_remote_disabled.diff +++ b/pkg/config/testdata/TestHookDiff/local_enabled_remote_disabled.diff @@ -1,7 +1,7 @@ diff remote[auth] local[auth] --- remote[auth] +++ local[auth] -@@ -24,20 +24,20 @@ +@@ -24,25 +24,25 @@ [hook] [hook.mfa_verification_attempt] @@ -27,5 +27,13 @@ diff remote[auth] local[auth] +enabled = true +uri = "pg-functions://postgres/public/sendEmail" secrets = "" + [hook.before_user_created] +-enabled = false +-uri = "pg-functions://postgres/public/beforeUserCreated" +-secrets = "" ++enabled = true ++uri = "http://example.com" ++secrets = "hash:ce62bb9bcced294fd4afe668f8ab3b50a89cf433093c526fffa3d0e46bf55252" [mfa] + max_enrolled_factors = 0 diff --git a/pkg/config/testdata/config.toml b/pkg/config/testdata/config.toml index ed0d508fc5..0077ec9563 100644 --- a/pkg/config/testdata/config.toml +++ b/pkg/config/testdata/config.toml @@ -204,6 +204,11 @@ timebox = "24h" # Force log out if the user has been inactive longer than the specified duration. inactivity_timeout = "8h" +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +[auth.hook.before_user_created] +enabled = true +uri = "pg-functions://postgres/auth/before-user-created-hook" + # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. [auth.hook.custom_access_token] enabled = true