From 1f7747b03d8777989060d5ebd7ffbc85b04edc9a Mon Sep 17 00:00:00 2001 From: barnarddt Date: Fri, 13 Sep 2024 09:09:54 +0200 Subject: [PATCH 1/2] feat: add after failure login webhook (#3580) --- driver/config/config.go | 5 +++ driver/config/config_test.go | 11 ++++++ .../config/stub/.kratos.webauthn.invalid.yaml | 34 ++++++++++++------- .../config/stub/.kratos.webauthn.origin.yaml | 34 ++++++++++++------- .../config/stub/.kratos.webauthn.origins.yaml | 34 ++++++++++++------- driver/config/stub/.kratos.yaml | 12 +++++-- driver/registry_default_login.go | 9 +++++ embedx/config.schema.json | 12 +++++++ internal/testhelpers/selfservice.go | 33 ++++++++++++++++++ selfservice/flow/login/handler.go | 3 ++ selfservice/flow/login/hook.go | 15 ++++++++ selfservice/flow/login/hook_test.go | 27 +++++++++++++-- selfservice/hook/error.go | 4 +++ selfservice/hook/web_hook.go | 12 +++++++ selfservice/hook/web_hook_integration_test.go | 32 +++++++++++++++++ 15 files changed, 233 insertions(+), 44 deletions(-) diff --git a/driver/config/config.go b/driver/config/config.go index 52762356fcc2..cd291905781f 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -139,6 +139,7 @@ const ( ViperKeySelfServiceLoginRequestLifespan = "selfservice.flows.login.lifespan" ViperKeySelfServiceLoginAfter = "selfservice.flows.login.after" ViperKeySelfServiceLoginBeforeHooks = "selfservice.flows.login.before.hooks" + ViperKeySelfServiceLoginFailedHooks = "selfservice.flows.login.failed.hooks" ViperKeySelfServiceErrorUI = "selfservice.flows.error.ui_url" ViperKeySelfServiceLogoutBrowserDefaultReturnTo = "selfservice.flows.logout.after." + DefaultBrowserReturnURL ViperKeySelfServiceSettingsURL = "selfservice.flows.settings.ui_url" @@ -702,6 +703,10 @@ func (p *Config) SelfServiceFlowLoginBeforeHooks(ctx context.Context) []SelfServ return p.selfServiceHooks(ctx, ViperKeySelfServiceLoginBeforeHooks) } +func (p *Config) SelfServiceFlowLoginFailedHooks(ctx context.Context) []SelfServiceHook { + return p.selfServiceHooks(ctx, ViperKeySelfServiceLoginFailedHooks) +} + func (p *Config) SelfServiceFlowRecoveryBeforeHooks(ctx context.Context) []SelfServiceHook { return p.selfServiceHooks(ctx, ViperKeySelfServiceRecoveryBeforeHooks) } diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 8f9dfaaf20ec..3e5ffb239fad 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -301,6 +301,17 @@ 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_login_hook"}`)}, + } + + hooks := p.SelfServiceFlowLoginFailedHooks(ctx) + + require.Len(t, hooks, 1) + assert.Equal(t, expHooks, hooks) + }) + for _, tc := range []struct { strategy string hooks []config.SelfServiceHook diff --git a/driver/config/stub/.kratos.webauthn.invalid.yaml b/driver/config/stub/.kratos.webauthn.invalid.yaml index a1230588c666..9328365d9298 100644 --- a/driver/config/stub/.kratos.webauthn.invalid.yaml +++ b/driver/config/stub/.kratos.webauthn.invalid.yaml @@ -104,7 +104,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -119,7 +119,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -136,7 +136,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_password_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet profile: @@ -145,7 +145,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_profile_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet hooks: @@ -153,7 +153,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_global_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -166,7 +166,15 @@ selfservice: config: url: https://test.kratos.ory.sh/before_login_hook method: POST - headers: + headers: + X-Custom-Header: test + failed: + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/failed_login_hook + method: POST + headers: X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to @@ -179,7 +187,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_password_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet auth: @@ -193,7 +201,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_oidc_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet - hook: revoke_active_sessions @@ -202,7 +210,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_global_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -216,7 +224,7 @@ selfservice: config: url: https://test.kratos.ory.sh/before_registration_hook method: GET - headers: + headers: X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to @@ -227,7 +235,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_password_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet hooks: @@ -235,7 +243,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_global_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet auth: @@ -251,7 +259,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_oidc_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet - hook: session diff --git a/driver/config/stub/.kratos.webauthn.origin.yaml b/driver/config/stub/.kratos.webauthn.origin.yaml index 95deec00910d..953129064fc5 100644 --- a/driver/config/stub/.kratos.webauthn.origin.yaml +++ b/driver/config/stub/.kratos.webauthn.origin.yaml @@ -100,7 +100,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -115,7 +115,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -132,7 +132,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_password_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet profile: @@ -141,7 +141,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_profile_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet hooks: @@ -149,7 +149,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_global_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -162,7 +162,15 @@ selfservice: config: url: https://test.kratos.ory.sh/before_login_hook method: POST - headers: + headers: + X-Custom-Header: test + failed: + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/failed_login_hook + method: POST + headers: X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to @@ -175,7 +183,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_password_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet auth: @@ -189,7 +197,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_oidc_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet - hook: revoke_active_sessions @@ -198,7 +206,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_global_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -212,7 +220,7 @@ selfservice: config: url: https://test.kratos.ory.sh/before_registration_hook method: GET - headers: + headers: X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to @@ -223,7 +231,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_password_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet hooks: @@ -231,7 +239,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_global_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet auth: @@ -247,7 +255,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_oidc_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet - hook: session diff --git a/driver/config/stub/.kratos.webauthn.origins.yaml b/driver/config/stub/.kratos.webauthn.origins.yaml index 9efeb34e0b02..f51ea8a33a9b 100644 --- a/driver/config/stub/.kratos.webauthn.origins.yaml +++ b/driver/config/stub/.kratos.webauthn.origins.yaml @@ -103,7 +103,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -118,7 +118,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -135,7 +135,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_password_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet profile: @@ -144,7 +144,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_profile_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet hooks: @@ -152,7 +152,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_settings_global_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -165,7 +165,15 @@ selfservice: config: url: https://test.kratos.ory.sh/before_login_hook method: POST - headers: + headers: + X-Custom-Header: test + failed: + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/failed_login_hook + method: POST + headers: X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to @@ -178,7 +186,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_password_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet auth: @@ -192,7 +200,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_oidc_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet - hook: revoke_active_sessions @@ -201,7 +209,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_login_global_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -215,7 +223,7 @@ selfservice: config: url: https://test.kratos.ory.sh/before_registration_hook method: GET - headers: + headers: X-Custom-Header: test after: default_browser_return_url: https://self-service/registration/return_to @@ -226,7 +234,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_password_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet hooks: @@ -234,7 +242,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_global_hook method: POST - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet auth: @@ -250,7 +258,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_registration_oidc_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet - hook: session diff --git a/driver/config/stub/.kratos.yaml b/driver/config/stub/.kratos.yaml index bc35439f8a53..2026b82b48b3 100644 --- a/driver/config/stub/.kratos.yaml +++ b/driver/config/stub/.kratos.yaml @@ -99,7 +99,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_recovery_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -114,7 +114,7 @@ selfservice: config: url: https://test.kratos.ory.sh/after_verification_hook method: GET - headers: + headers: X-Custom-Header: test body: /path/to/template.jsonnet @@ -163,6 +163,14 @@ selfservice: method: POST headers: X-Custom-Header: test + failed: + hooks: + - hook: web_hook + config: + url: https://test.kratos.ory.sh/failed_login_hook + method: POST + headers: + X-Custom-Header: test after: default_browser_return_url: https://self-service/login/return_to password: diff --git a/driver/registry_default_login.go b/driver/registry_default_login.go index f472b22f1e8b..aee8c52ec745 100644 --- a/driver/registry_default_login.go +++ b/driver/registry_default_login.go @@ -27,6 +27,15 @@ func (m *RegistryDefault) PreLoginHooks(ctx context.Context) (b []login.PreHookE return } +func (m *RegistryDefault) FailedLoginHooks(ctx context.Context) (b []login.FailedHookExecutor) { + for _, v := range m.getHooks("", m.Config().SelfServiceFlowLoginFailedHooks(ctx)) { + if hook, ok := v.(login.FailedHookExecutor); ok { + b = append(b, hook) + } + } + return +} + func (m *RegistryDefault) PostLoginHooks(ctx context.Context, credentialsType identity.CredentialsType) (b []login.PostHookExecutor) { for _, v := range m.getHooks(string(credentialsType), m.Config().SelfServiceFlowLoginAfterHooks(ctx, string(credentialsType))) { if hook, ok := v.(login.PostHookExecutor); ok { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index b7f0c468a963..7c771b8ae70c 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -876,6 +876,15 @@ } } }, + "selfServiceLoginFailed": { + "type": "object", + "additionalProperties": false, + "properties": { + "hooks": { + "$ref": "#/definitions/selfServiceHooks" + } + } + }, "selfServiceAfterLogin": { "type": "object", "additionalProperties": false, @@ -1307,6 +1316,9 @@ "before": { "$ref": "#/definitions/selfServiceBeforeLogin" }, + "failed": { + "$ref": "#/definitions/selfServiceLoginFailed" + }, "after": { "$ref": "#/definitions/selfServiceAfterLogin" } diff --git a/internal/testhelpers/selfservice.go b/internal/testhelpers/selfservice.go index 56903b04ac79..fb7c7fbc43ba 100644 --- a/internal/testhelpers/selfservice.go +++ b/internal/testhelpers/selfservice.go @@ -81,6 +81,33 @@ func TestSelfServicePreHook( } } +func TestSelfServiceFailedLoginHook( + configKey string, + makeRequestFail func(t *testing.T, ts *httptest.Server) (*http.Response, string), + newServer func(t *testing.T) *httptest.Server, + conf *config.Config, +) func(t *testing.T) { + ctx := context.Background() + return func(t *testing.T) { + t.Run("case=pass if hooks pass", func(t *testing.T) { + t.Cleanup(SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "webhook", Config: []byte(`{}`)}}) + + res, _ := makeRequestFail(t, newServer(t)) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + }) + + t.Run("case=err if hooks err", func(t *testing.T) { + t.Cleanup(SelfServiceHookConfigReset(t, conf)) + conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteLoginFailedHook": "err"}`)}}) + + res, body := makeRequestFail(t, newServer(t)) + assert.EqualValues(t, http.StatusInternalServerError, res.StatusCode, "%s", body) + assert.EqualValues(t, "err", body) + }) + } +} + func SelfServiceHookCreateFakeIdentity(t *testing.T, reg driver.Registry) *identity.Identity { i := SelfServiceHookFakeIdentity(t) require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) @@ -101,6 +128,8 @@ func SelfServiceHookConfigReset(t *testing.T, conf *config.Config) func() { return func() { conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter, nil) conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", nil) + conf.MustSet(ctx, config.ViperKeySelfServiceLoginFailedHooks, nil) + conf.MustSet(ctx, config.ViperKeySelfServiceLoginFailedHooks+".hooks", nil) conf.MustSet(ctx, config.ViperKeySelfServiceLoginBeforeHooks, nil) conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter, nil) conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".hooks", nil) @@ -181,6 +210,10 @@ func SelfServiceMakeLoginPreHookRequest(t *testing.T, ts *httptest.Server) (*htt return SelfServiceMakeHookRequest(t, ts, "/login/pre", false, url.Values{}) } +func SelfServiceMakeLoginFailedHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) { + return SelfServiceMakeHookRequest(t, ts, "/login/failed", false, url.Values{}) +} + func SelfServiceMakeLoginPostHookRequest(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*http.Response, string) { return SelfServiceMakeHookRequest(t, ts, "/login/post", asAPI, query) } diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go index fe76240b5221..52b911f7eaa7 100644 --- a/selfservice/flow/login/handler.go +++ b/selfservice/flow/login/handler.go @@ -853,6 +853,9 @@ func (h *Handler) updateLoginFlow(w http.ResponseWriter, r *http.Request, _ http continueLogin: if err := f.Valid(); err != nil { + if hookErr := h.d.LoginHookExecutor().FailedLoginHook(w, r, f); hookErr != nil { + h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, hookErr) + } h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err) return } diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index be25529241f0..e5c9801e690f 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -33,12 +33,17 @@ type ( ExecuteLoginPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error } + FailedHookExecutor interface { + ExecuteLoginFailedHook(w http.ResponseWriter, r *http.Request, a *Flow) error + } + PostHookExecutor interface { ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, g node.UiNodeGroup, a *Flow, s *session.Session) error } HooksProvider interface { PreLoginHooks(ctx context.Context) []PreHookExecutor + FailedLoginHooks(ctx context.Context) []FailedHookExecutor PostLoginHooks(ctx context.Context, credentialsType identity.CredentialsType) []PostHookExecutor } ) @@ -334,6 +339,16 @@ func (e *HookExecutor) PostLoginHook( return nil } +func (e *HookExecutor) FailedLoginHook(w http.ResponseWriter, r *http.Request, a *Flow) error { + for _, executor := range e.d.FailedLoginHooks(r.Context()) { + if err := executor.ExecuteLoginFailedHook(w, r, a); err != nil { + return err + } + } + + return nil +} + func (e *HookExecutor) PreLoginHook(w http.ResponseWriter, r *http.Request, a *Flow) error { for _, executor := range e.d.PreLoginHooks(r.Context()) { if err := executor.ExecuteLoginPreHook(w, r, a); err != nil { diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go index 7d7f8e158174..5789f89f04b3 100644 --- a/selfservice/flow/login/hook_test.go +++ b/selfservice/flow/login/hook_test.go @@ -10,15 +10,17 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" + + "github.com/ory/kratos/schema" + "github.com/stretchr/testify/require" "github.com/gobuffalo/httptest" "github.com/julienschmidt/httprouter" - "github.com/stretchr/testify/assert" - "github.com/tidwall/gjson" "github.com/ory/kratos/hydra" - "github.com/ory/kratos/schema" "github.com/ory/kratos/session" "github.com/ory/kratos/driver/config" @@ -36,6 +38,9 @@ func TestLoginExecutor(t *testing.T) { for _, strategy := range identity.AllCredentialTypes { strategy := strategy + if strategy == identity.CredentialsTypeCodeAuth { + continue + } t.Run("strategy="+strategy.String(), func(t *testing.T) { t.Parallel() @@ -56,6 +61,14 @@ func TestLoginExecutor(t *testing.T) { } }) + router.GET("/login/failed", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + loginFlow, err := login.NewFlow(conf, time.Minute, "", r, ft) + require.NoError(t, err) + if testhelpers.SelfServiceHookLoginErrorHandler(t, w, r, reg.LoginHookExecutor().FailedLoginHook(w, r, loginFlow)) { + _, _ = w.Write([]byte("ok")) + } + }) + router.GET("/login/post", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { loginFlow, err := login.NewFlow(conf, time.Minute, "", r, ft) require.NoError(t, err) @@ -375,6 +388,14 @@ func TestLoginExecutor(t *testing.T) { require.NotNil(t, err) require.True(t, requiresAAL2) }) + + t.Run("method=FailedLoginHook", testhelpers.TestSelfServiceFailedLoginHook( + config.ViperKeySelfServiceLoginFailedHooks, + testhelpers.SelfServiceMakeLoginFailedHookRequest, + func(t *testing.T) *httptest.Server { + return newServer(t, flow.TypeBrowser, nil) + }, + conf)) }) } } diff --git a/selfservice/hook/error.go b/selfservice/hook/error.go index bb396578e5f8..9c521f2517f6 100644 --- a/selfservice/hook/error.go +++ b/selfservice/hook/error.go @@ -72,6 +72,10 @@ func (e Error) ExecuteLoginPreHook(w http.ResponseWriter, r *http.Request, a *lo return e.err("ExecuteLoginPreHook", login.ErrHookAbortFlow) } +func (e Error) ExecuteLoginFailedHook(w http.ResponseWriter, r *http.Request, a *login.Flow) error { + return e.err("ExecuteLoginFailedHook", login.ErrHookAbortFlow) +} + func (e Error) ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, a *registration.Flow) error { return e.err("ExecuteRegistrationPreHook", registration.ErrHookAbortFlow) } diff --git a/selfservice/hook/web_hook.go b/selfservice/hook/web_hook.go index 6773e9ec4b89..30acbb8061bd 100644 --- a/selfservice/hook/web_hook.go +++ b/selfservice/hook/web_hook.go @@ -148,6 +148,18 @@ func (e *WebHook) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, }) } +func (e *WebHook) ExecuteLoginFailedHook(_ http.ResponseWriter, req *http.Request, flow *login.Flow) error { + return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteLoginFailedHook", 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) ExecuteVerificationPreHook(_ http.ResponseWriter, req *http.Request, flow *verification.Flow) error { return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteVerificationPreHook", func(ctx context.Context) error { return e.execute(ctx, &templateContext{ diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go index cae9659a285c..f59cfd361149 100644 --- a/selfservice/hook/web_hook_integration_test.go +++ b/selfservice/hook/web_hook_integration_test.go @@ -208,6 +208,16 @@ func TestWebHooks(t *testing.T) { return bodyWithFlowAndIdentityAndSessionAndTransientPayload(req, f, s, transientPayload) }, }, + { + uc: "Failed Login Hook", + createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteLoginFailedHook(nil, req, f.(*login.Flow)) + }, + expectedBody: func(req *http.Request, f flow.Flow, _ *session.Session) string { + return bodyWithFlowOnly(req, f) + }, + }, { uc: "Pre Registration Hook", createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, @@ -442,6 +452,28 @@ func TestWebHooks(t *testing.T) { }, expectedError: webhookError, }, + { + uc: "Failed Login Hook - no block", + createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteLoginFailedHook(nil, req, f.(*login.Flow)) + }, + webHookResponse: func() (int, []byte) { + return http.StatusOK, []byte{} + }, + expectedError: nil, + }, + { + uc: "Failed Login Hook - block", + createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { + return wh.ExecuteLoginFailedHook(nil, req, f.(*login.Flow)) + }, + webHookResponse: func() (int, []byte) { + return http.StatusBadRequest, webHookResponse + }, + expectedError: webhookError, + }, { uc: "Post Login Hook - no block", createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, From 6be134aec59e7be91782d6259c4e5cb30f35df7d Mon Sep 17 00:00:00 2001 From: barnarddt Date: Mon, 7 Oct 2024 14:06:52 +0200 Subject: [PATCH 2/2] fix: Remove debug strategy filter --- selfservice/flow/login/hook_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go index 5789f89f04b3..8bec05bdb55a 100644 --- a/selfservice/flow/login/hook_test.go +++ b/selfservice/flow/login/hook_test.go @@ -38,9 +38,6 @@ func TestLoginExecutor(t *testing.T) { for _, strategy := range identity.AllCredentialTypes { strategy := strategy - if strategy == identity.CredentialsTypeCodeAuth { - continue - } t.Run("strategy="+strategy.String(), func(t *testing.T) { t.Parallel()