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 webhook for failed registration #4145

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions driver/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,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"
Expand Down Expand Up @@ -733,6 +734,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)
Expand Down
10 changes: 10 additions & 0 deletions driver/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions driver/config/stub/.kratos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions driver/registry_default_registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,15 @@
}
}
},
"selfServiceFailedRegistration": {
"type": "object",
"additionalProperties": false,
"properties": {
"hooks": {
"$ref": "#/definitions/selfServiceHooks"
}
}
},
"selfServiceBeforeRegistration": {
"type": "object",
"additionalProperties": false,
Expand Down Expand Up @@ -1275,6 +1284,9 @@
"before": {
"$ref": "#/definitions/selfServiceBeforeRegistration"
},
"failed": {
"$ref": "#/definitions/selfServiceFailedRegistration"
},
"after": {
"$ref": "#/definitions/selfServiceAfterRegistration"
},
Expand Down
6 changes: 6 additions & 0 deletions internal/testhelpers/selfservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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{})
}
Expand Down
4 changes: 4 additions & 0 deletions selfservice/flow/registration/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
20 changes: 20 additions & 0 deletions selfservice/flow/registration/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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
}
Expand All @@ -43,6 +48,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
}
Expand All @@ -60,6 +66,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)
}
Expand Down Expand Up @@ -341,3 +351,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
}
8 changes: 8 additions & 0 deletions selfservice/flow/registration/hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions selfservice/hook/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
12 changes: 12 additions & 0 deletions selfservice/hook/web_hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions selfservice/hook/web_hook_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,16 @@ func TestWebHooks(t *testing.T) {
return bodyWithFlowOnly(req, f)
},
},
{
uc: "Failed Registration Hook",
createFlow: func() flow.Flow { return &registration.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 {
Expand Down Expand Up @@ -486,6 +496,29 @@ func TestWebHooks(t *testing.T) {
},
expectedError: webhookError,
},
{
uc: "Failed Registration Hook - no block",
createFlow: func() flow.Flow { return &registration.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 &registration.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 &registration.Flow{ID: x.NewUUID()} },
Expand Down
Loading