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: webhooks support setting client cookies #3716

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
109 changes: 80 additions & 29 deletions selfservice/hook/web_hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@
return &WebHook{deps: r, conf: c}
}

func (e *WebHook) ExecuteLoginPreHook(_ http.ResponseWriter, req *http.Request, flow *login.Flow) error {
func (e *WebHook) ExecuteLoginPreHook(w http.ResponseWriter, req *http.Request, flow *login.Flow) error {
return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteLoginPreHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
return e.execute(ctx, w, &templateContext{
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -131,9 +131,9 @@
})
}

func (e *WebHook) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, _ node.UiNodeGroup, flow *login.Flow, session *session.Session) error {
func (e *WebHook) ExecuteLoginPostHook(w 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{
return e.execute(ctx, w, &templateContext{
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -144,9 +144,9 @@
})
}

func (e *WebHook) ExecuteVerificationPreHook(_ http.ResponseWriter, req *http.Request, flow *verification.Flow) error {
func (e *WebHook) ExecuteVerificationPreHook(w http.ResponseWriter, req *http.Request, flow *verification.Flow) error {

Check warning on line 147 in selfservice/hook/web_hook.go

View check run for this annotation

Codecov / codecov/patch

selfservice/hook/web_hook.go#L147

Added line #L147 was not covered by tests
return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteVerificationPreHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
return e.execute(ctx, w, &templateContext{

Check warning on line 149 in selfservice/hook/web_hook.go

View check run for this annotation

Codecov / codecov/patch

selfservice/hook/web_hook.go#L149

Added line #L149 was not covered by tests
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -156,9 +156,9 @@
})
}

func (e *WebHook) ExecutePostVerificationHook(_ http.ResponseWriter, req *http.Request, flow *verification.Flow, id *identity.Identity) error {
func (e *WebHook) ExecutePostVerificationHook(w http.ResponseWriter, req *http.Request, flow *verification.Flow, id *identity.Identity) error {
return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecutePostVerificationHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
return e.execute(ctx, w, &templateContext{
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -169,9 +169,9 @@
})
}

func (e *WebHook) ExecuteRecoveryPreHook(_ http.ResponseWriter, req *http.Request, flow *recovery.Flow) error {
func (e *WebHook) ExecuteRecoveryPreHook(w http.ResponseWriter, req *http.Request, flow *recovery.Flow) error {

Check warning on line 172 in selfservice/hook/web_hook.go

View check run for this annotation

Codecov / codecov/patch

selfservice/hook/web_hook.go#L172

Added line #L172 was not covered by tests
return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteRecoveryPreHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
return e.execute(ctx, w, &templateContext{

Check warning on line 174 in selfservice/hook/web_hook.go

View check run for this annotation

Codecov / codecov/patch

selfservice/hook/web_hook.go#L174

Added line #L174 was not covered by tests
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -181,9 +181,9 @@
})
}

func (e *WebHook) ExecutePostRecoveryHook(_ http.ResponseWriter, req *http.Request, flow *recovery.Flow, session *session.Session) error {
func (e *WebHook) ExecutePostRecoveryHook(w http.ResponseWriter, req *http.Request, flow *recovery.Flow, session *session.Session) error {
return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecutePostRecoveryHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
return e.execute(ctx, w, &templateContext{
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -194,9 +194,9 @@
})
}

func (e *WebHook) ExecuteRegistrationPreHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow) error {
func (e *WebHook) ExecuteRegistrationPreHook(w http.ResponseWriter, req *http.Request, flow *registration.Flow) error {
return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteRegistrationPreHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
return e.execute(ctx, w, &templateContext{
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -206,13 +206,13 @@
})
}

func (e *WebHook) ExecutePostRegistrationPrePersistHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow, id *identity.Identity) error {
func (e *WebHook) ExecutePostRegistrationPrePersistHook(w 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
}

return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecutePostRegistrationPrePersistHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
return e.execute(ctx, w, &templateContext{
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -223,7 +223,7 @@
})
}

func (e *WebHook) ExecutePostRegistrationPostPersistHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow, session *session.Session) error {
func (e *WebHook) ExecutePostRegistrationPostPersistHook(w http.ResponseWriter, req *http.Request, flow *registration.Flow, session *session.Session) error {
if gjson.GetBytes(e.conf, "can_interrupt").Bool() || gjson.GetBytes(e.conf, "response.parse").Bool() {
return nil
}
Expand All @@ -233,7 +233,7 @@
ctx := context.WithoutCancel(req.Context())

return otelx.WithSpan(ctx, "selfservice.hook.WebHook.ExecutePostRegistrationPostPersistHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
return e.execute(ctx, w, &templateContext{
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -244,9 +244,9 @@
})
}

func (e *WebHook) ExecuteSettingsPreHook(_ http.ResponseWriter, req *http.Request, flow *settings.Flow) error {
func (e *WebHook) ExecuteSettingsPreHook(w http.ResponseWriter, req *http.Request, flow *settings.Flow) error {

Check warning on line 247 in selfservice/hook/web_hook.go

View check run for this annotation

Codecov / codecov/patch

selfservice/hook/web_hook.go#L247

Added line #L247 was not covered by tests
return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteSettingsPreHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
return e.execute(ctx, w, &templateContext{

Check warning on line 249 in selfservice/hook/web_hook.go

View check run for this annotation

Codecov / codecov/patch

selfservice/hook/web_hook.go#L249

Added line #L249 was not covered by tests
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -256,12 +256,12 @@
})
}

func (e *WebHook) ExecuteSettingsPostPersistHook(_ http.ResponseWriter, req *http.Request, flow *settings.Flow, id *identity.Identity, _ *session.Session) error {
func (e *WebHook) ExecuteSettingsPostPersistHook(w http.ResponseWriter, req *http.Request, flow *settings.Flow, id *identity.Identity, _ *session.Session) error {
if gjson.GetBytes(e.conf, "can_interrupt").Bool() || gjson.GetBytes(e.conf, "response.parse").Bool() {
return nil
}
return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteSettingsPostPersistHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
return e.execute(ctx, w, &templateContext{
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -272,12 +272,12 @@
})
}

func (e *WebHook) ExecuteSettingsPrePersistHook(_ http.ResponseWriter, req *http.Request, flow *settings.Flow, id *identity.Identity) error {
func (e *WebHook) ExecuteSettingsPrePersistHook(w http.ResponseWriter, req *http.Request, flow *settings.Flow, id *identity.Identity) error {
if !(gjson.GetBytes(e.conf, "can_interrupt").Bool() || gjson.GetBytes(e.conf, "response.parse").Bool()) {
return nil
}
return otelx.WithSpan(req.Context(), "selfservice.hook.WebHook.ExecuteSettingsPrePersistHook", func(ctx context.Context) error {
return e.execute(ctx, &templateContext{
return e.execute(ctx, w, &templateContext{
Flow: flow,
RequestHeaders: req.Header,
RequestMethod: req.Method,
Expand All @@ -288,7 +288,7 @@
})
}

func (e *WebHook) execute(ctx context.Context, data *templateContext) error {
func (e *WebHook) execute(ctx context.Context, w http.ResponseWriter, data *templateContext) error {
var (
httpClient = e.deps.HTTPClient(ctx)
ignoreResponse = gjson.GetBytes(e.conf, "response.ignore").Bool()
Expand Down Expand Up @@ -391,7 +391,7 @@
if resp.StatusCode >= http.StatusBadRequest {
span.SetStatus(codes.Error, "HTTP status code >= 400")
if canInterrupt || parseResponse {
if err := parseWebhookResponse(resp, data.Identity); err != nil {
if err := parseWebhookResponse(resp, w, data.Identity); err != nil {
return err
}
}
Expand All @@ -405,7 +405,7 @@
}

if parseResponse {
return parseWebhookResponse(resp, data.Identity)
return parseWebhookResponse(resp, w, data.Identity)
}
return nil
}
Expand All @@ -420,20 +420,71 @@
return nil
}

func parseWebhookResponse(resp *http.Response, id *identity.Identity) (err error) {
// A near 1 to 1 copy of the http.Cookie struct but with json struct tags
type hookResponseCookie struct {
Name string `json:"name"`
Value string `json:"value"`

Path string `json:"path"` // optional
Domain string `json:"domain"` // optional
Expires time.Time `json:"expires"` // optional

// MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// MaxAge>0 means Max-Age attribute present and given in seconds
MaxAge int `json:"max_age"`
Secure bool `json:"secure"`
HttpOnly bool `json:"http_only"`

// Options are: "lax", "strict", "none", or omitted/empty ("") for the browser default
SameSite string `json:"same_site"`
}

func (c *hookResponseCookie) AsCookie() *http.Cookie {
sameSite := http.SameSiteDefaultMode
switch c.SameSite {
case "lax":
sameSite = http.SameSiteLaxMode
case "strict":
sameSite = http.SameSiteStrictMode
case "none":
sameSite = http.SameSiteNoneMode

Check warning on line 451 in selfservice/hook/web_hook.go

View check run for this annotation

Codecov / codecov/patch

selfservice/hook/web_hook.go#L443-L451

Added lines #L443 - L451 were not covered by tests
}

return &http.Cookie{
Name: c.Name,
Value: c.Value,
Path: c.Path,
Domain: c.Domain,
Expires: c.Expires,
MaxAge: c.MaxAge,
Secure: c.Secure,
HttpOnly: c.HttpOnly,
SameSite: sameSite,

Check warning on line 463 in selfservice/hook/web_hook.go

View check run for this annotation

Codecov / codecov/patch

selfservice/hook/web_hook.go#L454-L463

Added lines #L454 - L463 were not covered by tests
}
}

func parseWebhookResponse(resp *http.Response, w http.ResponseWriter, id *identity.Identity) (err error) {
if resp == nil {
return errors.Errorf("empty response provided from the webhook")
}

if resp.StatusCode == http.StatusOK {
type localIdentity identity.Identity
var hookResponse struct {
Identity *localIdentity `json:"identity"`
Identity *localIdentity `json:"identity"`
Cookies []hookResponseCookie `json:"cookies"`
}
if err := json.NewDecoder(resp.Body).Decode(&hookResponse); err != nil {
return errors.Wrap(err, "webhook response could not be unmarshalled properly from JSON")
}

if len(hookResponse.Cookies) > 0 {
for i := range hookResponse.Cookies {
http.SetCookie(w, hookResponse.Cookies[i].AsCookie())

Check warning on line 484 in selfservice/hook/web_hook.go

View check run for this annotation

Codecov / codecov/patch

selfservice/hook/web_hook.go#L483-L484

Added lines #L483 - L484 were not covered by tests
}
}

if hookResponse.Identity == nil {
return nil
}
Expand Down
Loading