From 92d88b2def775f2641169e699e1afa3b73d24fcf Mon Sep 17 00:00:00 2001 From: barnarddt Date: Mon, 7 Oct 2024 16:33:34 +0200 Subject: [PATCH] feat: Add webhook for failed registration --- driver/config/config.go | 5 +++ driver/config/config_test.go | 10 ++++++ driver/config/stub/.kratos.yaml | 8 +++++ driver/registry_default_registration.go | 9 +++++ embedx/config.schema.json | 12 +++++++ internal/testhelpers/selfservice.go | 6 ++++ selfservice/flow/registration/handler.go | 4 +++ selfservice/flow/registration/hook.go | 20 +++++++++++ selfservice/flow/registration/hook_test.go | 8 +++++ selfservice/hook/error.go | 4 +++ selfservice/hook/web_hook.go | 12 +++++++ selfservice/hook/web_hook_integration_test.go | 33 +++++++++++++++++++ 12 files changed, 131 insertions(+) diff --git a/driver/config/config.go b/driver/config/config.go index 52762356fcc2..6ba207f0377a 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -133,6 +133,7 @@ const ( ViperKeySelfServiceRegistrationRequestLifespan = "selfservice.flows.registration.lifespan" ViperKeySelfServiceRegistrationAfter = "selfservice.flows.registration.after" ViperKeySelfServiceRegistrationBeforeHooks = "selfservice.flows.registration.before.hooks" + ViperKeySelfServiceRegistrationFailedHooks = "selfservice.flows.registration.failed.hooks" ViperKeySelfServiceLoginUI = "selfservice.flows.login.ui_url" ViperKeySelfServiceLoginFlowStyle = "selfservice.flows.login.style" ViperKeySecurityAccountEnumerationMitigate = "security.account_enumeration.mitigate" @@ -731,6 +732,10 @@ func (p *Config) SelfServiceFlowRegistrationBeforeHooks(ctx context.Context) []S return hooks } +func (p *Config) SelfServiceFlowRegistrationFailedHooks(ctx context.Context) []SelfServiceHook { + return p.selfServiceHooks(ctx, ViperKeySelfServiceRegistrationFailedHooks) +} + func (p *Config) selfServiceHooks(ctx context.Context, key string) []SelfServiceHook { pp := p.GetProvider(ctx) val := pp.Get(key) diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 8f9dfaaf20ec..22be966e5c7b 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -245,6 +245,16 @@ func TestViperProvider(t *testing.T) { // assert.JSONEq(t, `{"allow_user_defined_redirect":false,"default_redirect_url":"http://test.kratos.ory.sh:4000/"}`, string(hook.Config)) }) + t.Run("hook=failed", func(t *testing.T) { + expHooks := []config.SelfServiceHook{ + {Name: "web_hook", Config: json.RawMessage(`{"headers":{"X-Custom-Header":"test"},"method":"POST","url":"https://test.kratos.ory.sh/failed_registration_hook"}`)}, + } + + hooks := p.SelfServiceFlowRegistrationFailedHooks(ctx) + + assert.Equal(t, expHooks, hooks) + }) + for _, tc := range []struct { strategy string hooks []config.SelfServiceHook diff --git a/driver/config/stub/.kratos.yaml b/driver/config/stub/.kratos.yaml index bc35439f8a53..46c414ac93f1 100644 --- a/driver/config/stub/.kratos.yaml +++ b/driver/config/stub/.kratos.yaml @@ -213,6 +213,14 @@ selfservice: method: GET headers: X-Custom-Header: test + failed: + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/failed_registration_hook + method: POST + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to password: diff --git a/driver/registry_default_registration.go b/driver/registry_default_registration.go index 89ed5e656c74..3c1faaf7427b 100644 --- a/driver/registry_default_registration.go +++ b/driver/registry_default_registration.go @@ -60,6 +60,15 @@ func (m *RegistryDefault) PreRegistrationHooks(ctx context.Context) (b []registr return } +func (m *RegistryDefault) FailedRegistrationHooks(ctx context.Context) (b []registration.FailedHookExecutor) { + for _, v := range m.getHooks("", m.Config().SelfServiceFlowRegistrationFailedHooks(ctx)) { + if hook, ok := v.(registration.FailedHookExecutor); ok { + b = append(b, hook) + } + } + return +} + func (m *RegistryDefault) RegistrationExecutor() *registration.HookExecutor { if m.selfserviceRegistrationExecutor == nil { m.selfserviceRegistrationExecutor = registration.NewHookExecutor(m) diff --git a/embedx/config.schema.json b/embedx/config.schema.json index b7f0c468a963..c4e8a9b5cf26 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -933,6 +933,15 @@ } } }, + "selfServiceFailedRegistration": { + "type": "object", + "additionalProperties": false, + "properties": { + "hooks": { + "$ref": "#/definitions/selfServiceHooks" + } + } + }, "selfServiceBeforeRegistration": { "type": "object", "additionalProperties": false, @@ -1268,6 +1277,9 @@ "before": { "$ref": "#/definitions/selfServiceBeforeRegistration" }, + "failed": { + "$ref": "#/definitions/selfServiceFailedRegistration" + }, "after": { "$ref": "#/definitions/selfServiceAfterRegistration" }, diff --git a/internal/testhelpers/selfservice.go b/internal/testhelpers/selfservice.go index 56903b04ac79..d0de2388a914 100644 --- a/internal/testhelpers/selfservice.go +++ b/internal/testhelpers/selfservice.go @@ -107,6 +107,8 @@ func SelfServiceHookConfigReset(t *testing.T, conf *config.Config) func() { conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter, nil) conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter+".hooks", nil) conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationBeforeHooks, nil) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationFailedHooks, nil) + conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationFailedHooks+".hooks", nil) conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter, nil) conf.MustSet(ctx, config.ViperKeySelfServiceSettingsAfter+".hooks", nil) } @@ -189,6 +191,10 @@ func SelfServiceMakeRegistrationPreHookRequest(t *testing.T, ts *httptest.Server return SelfServiceMakeHookRequest(t, ts, "/registration/pre", false, url.Values{}) } +func SelfServiceMakeRegistrationFailedHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) { + return SelfServiceMakeHookRequest(t, ts, "/registration/failed", false, url.Values{}) +} + func SelfServiceMakeSettingsPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) { return SelfServiceMakeHookRequest(t, ts, "/settings/pre", false, url.Values{}) } diff --git a/selfservice/flow/registration/handler.go b/selfservice/flow/registration/handler.go index 8cfe59e4d6d9..6219bb23e787 100644 --- a/selfservice/flow/registration/handler.go +++ b/selfservice/flow/registration/handler.go @@ -650,6 +650,10 @@ func (h *Handler) updateRegistrationFlow(w http.ResponseWriter, r *http.Request, } else if errors.Is(err, flow.ErrCompletedByStrategy) { return } else if err != nil { + // Fire after failure webhook + if hookErr := h.d.RegistrationExecutor().FailedRegistrationHook(w, r, f); hookErr != nil { + h.d.RegistrationFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), hookErr) + } h.d.RegistrationFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), err) return } diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go index c1c7b7ed4b2c..6d564e7b2de4 100644 --- a/selfservice/flow/registration/hook.go +++ b/selfservice/flow/registration/hook.go @@ -32,6 +32,11 @@ type ( } PreHookExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow) error + FailedHookExecutor interface { + ExecuteFailedRegistrationHook(w http.ResponseWriter, r *http.Request, a *Flow) error + } + FailedHookExecutorFunc func(w http.ResponseWriter, r *http.Request, a *Flow) error + PostHookPostPersistExecutor interface { ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error } @@ -44,6 +49,7 @@ type ( HooksProvider interface { PreRegistrationHooks(ctx context.Context) []PreHookExecutor + FailedRegistrationHooks(ctx context.Context) []FailedHookExecutor PostRegistrationPrePersistHooks(ctx context.Context, credentialsType identity.CredentialsType) []PostHookPrePersistExecutor PostRegistrationPostPersistHooks(ctx context.Context, credentialsType identity.CredentialsType) []PostHookPostPersistExecutor } @@ -61,6 +67,10 @@ func (f PreHookExecutorFunc) ExecuteRegistrationPreHook(w http.ResponseWriter, r return f(w, r, a) } +func (f FailedHookExecutorFunc) ExecuteFailedRegistrationHook(w http.ResponseWriter, r *http.Request, a *Flow) error { + return f(w, r, a) +} + func (f PostHookPostPersistExecutorFunc) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *Flow, s *session.Session) error { return f(w, r, a, s) } @@ -343,3 +353,13 @@ func (e *HookExecutor) PreRegistrationHook(w http.ResponseWriter, r *http.Reques return nil } + +func (e *HookExecutor) FailedRegistrationHook(w http.ResponseWriter, r *http.Request, a *Flow) error { + for _, executor := range e.d.FailedRegistrationHooks(r.Context()) { + if err := executor.ExecuteFailedRegistrationHook(w, r, a); err != nil { + return err + } + } + + return nil +} diff --git a/selfservice/flow/registration/hook_test.go b/selfservice/flow/registration/hook_test.go index 9e60b33f1f52..56b4e661bd1e 100644 --- a/selfservice/flow/registration/hook_test.go +++ b/selfservice/flow/registration/hook_test.go @@ -55,6 +55,14 @@ func TestRegistrationExecutor(t *testing.T) { } }) + router.GET("/registration/failed", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + f, err := registration.NewFlow(conf, time.Minute, x.FakeCSRFToken, r, ft) + require.NoError(t, err) + if handleErr(t, w, r, reg.RegistrationHookExecutor().FailedRegistrationHook(w, r, f)) { + _, _ = w.Write([]byte("ok")) + } + }) + router.GET("/registration/post", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { if i == nil { i = testhelpers.SelfServiceHookFakeIdentity(t) diff --git a/selfservice/hook/error.go b/selfservice/hook/error.go index bb396578e5f8..c17e5a12045c 100644 --- a/selfservice/hook/error.go +++ b/selfservice/hook/error.go @@ -76,6 +76,10 @@ func (e Error) ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request return e.err("ExecuteRegistrationPreHook", registration.ErrHookAbortFlow) } +func (e Error) ExecuteRegistrationFailedHook(w http.ResponseWriter, r *http.Request, a *registration.Flow) error { + return e.err("ExecuteRegistrationFailedHook", registration.ErrHookAbortFlow) +} + func (e Error) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, r *http.Request, a *registration.Flow, s *session.Session) error { return e.err("ExecutePostRegistrationPostPersistHook", registration.ErrHookAbortFlow) } diff --git a/selfservice/hook/web_hook.go b/selfservice/hook/web_hook.go index 6773e9ec4b89..aafe6e37f5b4 100644 --- a/selfservice/hook/web_hook.go +++ b/selfservice/hook/web_hook.go @@ -210,6 +210,18 @@ func (e *WebHook) ExecuteRegistrationPreHook(_ http.ResponseWriter, req *http.Re }) } +func (e *WebHook) ExecuteRegistrationFailedHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow) error { + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteRegistrationFailedHook", func(ctx context.Context) error { + return e.execute(ctx, &templateContext{ + Flow: flow, + RequestHeaders: req.Header, + RequestMethod: req.Method, + RequestURL: x.RequestURL(req).String(), + RequestCookies: cookies(req), + }) + }) +} + func (e *WebHook) ExecutePostRegistrationPrePersistHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow, id *identity.Identity) error { if !(gjson.GetBytes(e.conf, "can_interrupt").Bool() || gjson.GetBytes(e.conf, "response.parse").Bool()) { return nil diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go index cae9659a285c..dbc462c35f41 100644 --- a/selfservice/hook/web_hook_integration_test.go +++ b/selfservice/hook/web_hook_integration_test.go @@ -218,6 +218,16 @@ func TestWebHooks(t *testing.T) { return bodyWithFlowOnly(req, f) }, }, + { + uc: "Failed Registration Hook", + createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteRegistrationFailedHook(nil, req, f.(*registration.Flow)) + }, + expectedBody: func(req *http.Request, f flow.Flow, _ *session.Session) string { + return bodyWithFlowOnly(req, f) + }, + }, { uc: "Post Registration Hook", createFlow: func() flow.Flow { @@ -486,6 +496,29 @@ func TestWebHooks(t *testing.T) { }, expectedError: webhookError, }, + { + uc: "Failed Registration Hook - no block", + createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteRegistrationFailedHook(nil, req, f.(*registration.Flow)) + }, + webHookResponse: func() (int, []byte) { + return http.StatusOK, []byte{} + }, + expectedError: nil, + }, + + { + uc: "Failed Registration Hook - block", + createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteRegistrationFailedHook(nil, req, f.(*registration.Flow)) + }, + webHookResponse: func() (int, []byte) { + return http.StatusBadRequest, webHookResponse + }, + expectedError: webhookError, + }, { uc: "Post Registration Post Persist Hook - no block", createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} },