Skip to content

Commit

Permalink
feat: add a support for after submit webhooks in the login flow
Browse files Browse the repository at this point in the history
  • Loading branch information
adpaste committed Sep 3, 2024
1 parent e2df1fc commit bd2a08f
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 0 deletions.
5 changes: 5 additions & 0 deletions driver/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const (
ViperKeySelfServiceLoginRequestLifespan = "selfservice.flows.login.lifespan"
ViperKeySelfServiceLoginAfter = "selfservice.flows.login.after"
ViperKeySelfServiceLoginBeforeHooks = "selfservice.flows.login.before.hooks"
ViperKeySelfServiceLoginAfterSubmitHooks = "selfservice.flows.login.after_submit.hooks"
ViperKeySelfServiceErrorUI = "selfservice.flows.error.ui_url"
ViperKeySelfServiceLogoutBrowserDefaultReturnTo = "selfservice.flows.logout.after." + DefaultBrowserReturnURL
ViperKeySelfServiceSettingsURL = "selfservice.flows.settings.ui_url"
Expand Down Expand Up @@ -702,6 +703,10 @@ func (p *Config) SelfServiceFlowLoginBeforeHooks(ctx context.Context) []SelfServ
return p.selfServiceHooks(ctx, ViperKeySelfServiceLoginBeforeHooks)
}

func (p *Config) SelfServiceFlowLoginAfterSubmitHooks(ctx context.Context) []SelfServiceHook {
return p.selfServiceHooks(ctx, ViperKeySelfServiceLoginAfterSubmitHooks)
}

func (p *Config) SelfServiceFlowRecoveryBeforeHooks(ctx context.Context) []SelfServiceHook {
return p.selfServiceHooks(ctx, ViperKeySelfServiceRecoveryBeforeHooks)
}
Expand Down
9 changes: 9 additions & 0 deletions driver/registry_default_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ func (m *RegistryDefault) PreLoginHooks(ctx context.Context) (b []login.PreHookE
return
}

func (m *RegistryDefault) AfterSubmitLoginHooks(ctx context.Context) (b []login.AfterSubmitHookExecutor) {
for _, v := range m.getHooks("", m.Config().SelfServiceFlowLoginAfterSubmitHooks(ctx)) {
if hook, ok := v.(login.AfterSubmitHookExecutor); 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 {
Expand Down
9 changes: 9 additions & 0 deletions embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,15 @@
},
"after": {
"$ref": "#/definitions/selfServiceAfterLogin"
},
"after_submit": {
"type": "object",
"additionalProperties": false,
"properties": {
"hooks": {
"$ref": "#/definitions/selfServiceHooks"
}
}
}
}
},
Expand Down
49 changes: 49 additions & 0 deletions internal/testhelpers/selfservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,50 @@ func TestSelfServicePreHook(
}
}

func TestSelfServiceAfterSubmitHook(
configKey string,
makeRequestPost func(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*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 without hooks", func(t *testing.T) {
t.Cleanup(SelfServiceHookConfigReset(t, conf))

res, _ := makeRequestPost(t, newServer(t), false, url.Values{})
assert.EqualValues(t, http.StatusOK, res.StatusCode)
})

t.Run("case=pass if hooks pass", func(t *testing.T) {
t.Cleanup(SelfServiceHookConfigReset(t, conf))
conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "err", Config: []byte(`{}`)}})

res, _ := makeRequestPost(t, newServer(t), false, url.Values{})
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(`{"ExecuteAfterSubmitLoginHook": "err"}`)}})

res, body := makeRequestPost(t, newServer(t), false, url.Values{})
assert.EqualValues(t, http.StatusInternalServerError, res.StatusCode, "%s", body)
assert.EqualValues(t, "err", body)
})

t.Run("case=abort if hooks aborts", func(t *testing.T) {
t.Cleanup(SelfServiceHookConfigReset(t, conf))
conf.MustSet(ctx, configKey, []config.SelfServiceHook{{Name: "err", Config: []byte(`{"ExecuteAfterSubmitLoginHook": "abort"}`)}})

res, body := makeRequestPost(t, newServer(t), false, url.Values{})
assert.EqualValues(t, http.StatusOK, res.StatusCode)
assert.Empty(t, body)
})

}
}

func SelfServiceHookCreateFakeIdentity(t *testing.T, reg driver.Registry) *identity.Identity {
i := SelfServiceHookFakeIdentity(t)
require.NoError(t, reg.IdentityManager().Create(context.Background(), i))
Expand All @@ -102,6 +146,7 @@ func SelfServiceHookConfigReset(t *testing.T, conf *config.Config) func() {
conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter, nil)
conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfter+".hooks", nil)
conf.MustSet(ctx, config.ViperKeySelfServiceLoginBeforeHooks, nil)
conf.MustSet(ctx, config.ViperKeySelfServiceLoginAfterSubmitHooks, nil)
conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter, nil)
conf.MustSet(ctx, config.ViperKeySelfServiceRecoveryAfter+".hooks", nil)
conf.MustSet(ctx, config.ViperKeySelfServiceRegistrationAfter, nil)
Expand Down Expand Up @@ -185,6 +230,10 @@ func SelfServiceMakeLoginPostHookRequest(t *testing.T, ts *httptest.Server, asAP
return SelfServiceMakeHookRequest(t, ts, "/login/post", asAPI, query)
}

func SelfServiceMakeLoginAfterSubmitHookRequest(t *testing.T, ts *httptest.Server, asAPI bool, query url.Values) (*http.Response, string) {
return SelfServiceMakeHookRequest(t, ts, "/login/submit", asAPI, query)
}

func SelfServiceMakeRegistrationPreHookRequest(t *testing.T, ts *httptest.Server) (*http.Response, string) {
return SelfServiceMakeHookRequest(t, ts, "/registration/pre", false, url.Values{})
}
Expand Down
5 changes: 5 additions & 0 deletions selfservice/flow/login/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,11 @@ func (h *Handler) updateLoginFlow(w http.ResponseWriter, r *http.Request, _ http
}

continueLogin:
if err := h.d.LoginHookExecutor().AfterSubmitLoginHook(w, r, f); err != nil {
h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err)
return
}

if err := f.Valid(); err != nil {
h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err)
return
Expand Down
15 changes: 15 additions & 0 deletions selfservice/flow/login/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,17 @@ type (
ExecuteLoginPreHook(w http.ResponseWriter, r *http.Request, a *Flow) error
}

AfterSubmitHookExecutor interface {
ExecuteAfterSubmitLoginHook(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
AfterSubmitLoginHooks(ctx context.Context) []AfterSubmitHookExecutor
PostLoginHooks(ctx context.Context, credentialsType identity.CredentialsType) []PostHookExecutor
}
)
Expand Down Expand Up @@ -334,6 +339,16 @@ func (e *HookExecutor) PostLoginHook(
return nil
}

func (e *HookExecutor) AfterSubmitLoginHook(w http.ResponseWriter, r *http.Request, a *Flow) error {
for _, executor := range e.d.AfterSubmitLoginHooks(r.Context()) {
if err := executor.ExecuteAfterSubmitLoginHook(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 {
Expand Down
26 changes: 26 additions & 0 deletions selfservice/flow/login/hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ func TestLoginExecutor(t *testing.T) {
}
})

router.GET("/login/submit", 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().AfterSubmitLoginHook(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)
Expand Down Expand Up @@ -357,6 +365,15 @@ func TestLoginExecutor(t *testing.T) {
},
conf,
))

t.Run("method=AfterSubmitHook", testhelpers.TestSelfServiceAfterSubmitHook(
config.ViperKeySelfServiceLoginAfterSubmitHooks,
testhelpers.SelfServiceMakeLoginAfterSubmitHookRequest,
func(t *testing.T) *httptest.Server {
return newServer(t, flow.TypeAPI, nil)
},
conf,
))
})

t.Run("type=browser", func(t *testing.T) {
Expand All @@ -368,6 +385,15 @@ func TestLoginExecutor(t *testing.T) {
},
conf,
))

t.Run("method=AfterSubmitHook", testhelpers.TestSelfServiceAfterSubmitHook(
config.ViperKeySelfServiceLoginAfterSubmitHooks,
testhelpers.SelfServiceMakeLoginAfterSubmitHookRequest,
func(t *testing.T) *httptest.Server {
return newServer(t, flow.TypeBrowser, nil)
},
conf,
))
})

t.Run("requiresAAL2 should return true if there's an error", func(t *testing.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 @@ -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) ExecuteAfterSubmitLoginHook(w http.ResponseWriter, r *http.Request, a *login.Flow) error {
return e.err("ExecuteAfterSubmitLoginHook", login.ErrHookAbortFlow)
}

func (e Error) ExecuteRegistrationPreHook(w http.ResponseWriter, r *http.Request, a *registration.Flow) error {
return e.err("ExecuteRegistrationPreHook", 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 @@ -134,6 +134,18 @@ func (e *WebHook) ExecuteLoginPreHook(_ http.ResponseWriter, req *http.Request,
})
}

func (e *WebHook) ExecuteAfterSubmitLoginHook(_ http.ResponseWriter, req *http.Request, flow *login.Flow) error {
return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteAfterSubmitLoginHook", 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) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, _ node.UiNodeGroup, flow *login.Flow, session *session.Session) error {
return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteLoginPostHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
Expand Down

0 comments on commit bd2a08f

Please sign in to comment.