From 5bea7509b0fec22cc6d431ae2c522a92f41ff97c Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Thu, 20 Jan 2022 12:12:18 +0100 Subject: [PATCH] feat(security): add option to disallow private IP ranges in webhooks Closes #2152 --- docs/docs/guides/production.md | 15 + driver/config/config.go | 5 + driver/config/config_test.go | 6 + driver/registry_default.go | 17 + embedx/config.schema.json | 20 + go.mod | 4 +- go.sum | 8 +- persistence/sql/testhelpers/network.go | 4 +- selfservice/hook/web_hook.go | 77 ++-- selfservice/hook/web_hook_integration_test.go | 394 ++++++++++++++++++ selfservice/hook/web_hook_test.go | 386 +---------------- x/http.go | 7 + 12 files changed, 532 insertions(+), 411 deletions(-) create mode 100644 selfservice/hook/web_hook_integration_test.go diff --git a/docs/docs/guides/production.md b/docs/docs/guides/production.md index d37a06680030..f16faff30a4b 100644 --- a/docs/docs/guides/production.md +++ b/docs/docs/guides/production.md @@ -19,6 +19,21 @@ CockroachDB. Do not use SQLite in production! When preparing for production it is paramount to omit the `--dev` flag from `kratos serve`. +### HTTP Clients + +In some scenarios you might want to disallow HTTP calls to private IP ranges. +To configure this feature, set the following configuration: + +```yaml +clients: + http: + disallow_private_ip_ranges: true +``` + +If enabled, all outgoing HTTP calls done by Ory Kratos will be checked whether +they are against a private IP range. If that is the case, the request +will fail with an error. + ### Admin API Never expose the Ory Kratos Admin API to the internet unsecured. Always require diff --git a/driver/config/config.go b/driver/config/config.go index fc3dd0be3f45..16acbe40a9da 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -150,6 +150,7 @@ const ( ViperKeyWebAuthnRPID = "selfservice.methods.webauthn.config.rp.id" ViperKeyWebAuthnRPOrigin = "selfservice.methods.webauthn.config.rp.origin" ViperKeyWebAuthnRPIcon = "selfservice.methods.webauthn.config.rp.issuer" + ViperKeyClientHTTPNoPrivateIPRanges = "clients.http.disallow_private_ip_ranges" ViperKeyVersion = "version" ) @@ -514,6 +515,10 @@ func (p *Config) DisableAPIFlowEnforcement() bool { return false } +func (p *Config) ClientHTTPNoPrivateIPRanges() bool { + return p.p.Bool(ViperKeyClientHTTPNoPrivateIPRanges) +} + func (p *Config) SelfServiceFlowRegistrationEnabled() bool { return p.p.Bool(ViperKeySelfServiceRegistrationEnabled) } diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 523efd32c018..a278f79b31c5 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -46,6 +46,12 @@ func TestViperProvider(t *testing.T) { p := config.MustNew(t, logrusx.New("", ""), os.Stderr, configx.WithConfigFiles("stub/.kratos.yaml")) + t.Run("gourp=client config", func(t *testing.T) { + assert.False(t, p.ClientHTTPNoPrivateIPRanges(), "Should not have private IP ranges disabled per default") + p.MustSet(config.ViperKeyClientHTTPNoPrivateIPRanges, true) + assert.True(t, p.ClientHTTPNoPrivateIPRanges(), "Should disallow private IP ranges if set") + }) + t.Run("group=urls", func(t *testing.T) { assert.Equal(t, "http://test.kratos.ory.sh/login", p.SelfServiceFlowLoginUI().String()) assert.Equal(t, "http://test.kratos.ory.sh/settings", p.SelfServiceFlowSettingsUI().String()) diff --git a/driver/registry_default.go b/driver/registry_default.go index cd8f53e9b599..fc59c3e145fd 100644 --- a/driver/registry_default.go +++ b/driver/registry_default.go @@ -7,6 +7,10 @@ import ( "sync" "time" + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/x/httpx" + "github.com/gobuffalo/pop/v6" "github.com/ory/nosurf" @@ -679,3 +683,16 @@ func (m *RegistryDefault) PrometheusManager() *prometheus.MetricsManager { } return m.pmm } + +func (m *RegistryDefault) HTTPClient(ctx context.Context) *retryablehttp.Client { + opts := []httpx.ResilientOptions{ + httpx.ResilientClientWithLogger(m.Logger()), + httpx.ResilientClientWithMaxRetry(2), + httpx.ResilientClientWithConnectionTimeout(30 * time.Second), + } + if m.Config(ctx).ClientHTTPNoPrivateIPRanges() { + opts = append(opts, httpx.ResilientClientDisallowInternalIPs()) + + } + return httpx.NewResilientClient(opts...) +} diff --git a/embedx/config.schema.json b/embedx/config.schema.json index eafad4ae6984..a5ed40107509 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2113,6 +2113,26 @@ "type": "string" }, "description": "This is a CLI flag and environment variable and can not be set using the config file." + }, + "clients": { + "title": "Global outgoing network settings", + "description": "Configure how outgoing network calls behave.", + "type": "object", + "properties": { + "http": { + "title": "Global HTTP client configuration", + "description": "Configure how outgoing HTTP calls behave.", + "type": "object", + "properties": { + "disallow_private_ip_ranges": { + "title": "Disallow private IP ranges", + "description": "Disallow all outgoing HTTP calls to private IP ranges. This feature can help protect against SSRF attacks.", + "type": "boolean", + "default": false + } + } + } + } } }, "allOf": [ diff --git a/go.mod b/go.mod index 1d65da994559..7d3fa9bb5150 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( github.com/inhies/go-bytesize v0.0.0-20210819104631-275770b98743 github.com/jteeuwen/go-bindata v3.0.7+incompatible github.com/julienschmidt/httprouter v1.3.0 - github.com/knadh/koanf v1.3.3 + github.com/knadh/koanf v1.4.0 github.com/luna-duclos/instrumentedsql v1.1.3 github.com/luna-duclos/instrumentedsql/opentracing v0.0.0-20201103091713-40d03108b6f4 github.com/mattn/goveralls v0.0.7 @@ -77,7 +77,7 @@ require ( github.com/ory/kratos-client-go v0.6.3-alpha.1 github.com/ory/mail/v3 v3.0.0 github.com/ory/nosurf v1.2.7 - github.com/ory/x v0.0.330 + github.com/ory/x v0.0.334 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.3.0 diff --git a/go.sum b/go.sum index fac5356690d4..2332bd3d3db7 100644 --- a/go.sum +++ b/go.sum @@ -83,7 +83,6 @@ github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugX github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= @@ -399,8 +398,6 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczC github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/cli v20.10.8+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/cli v20.10.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v20.10.11+incompatible h1:tXU1ezXcruZQRrMP8RN2z9N91h+6egZTS1gsPsKantc= github.com/docker/cli v20.10.11+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= @@ -1544,7 +1541,6 @@ github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnh github.com/ory/dockertest/v3 v3.5.4/go.mod h1:J8ZUbNB2FOhm1cFZW9xBpDsODqsSWcyYgtJYVPcnF70= github.com/ory/dockertest/v3 v3.6.3/go.mod h1:EFLcVUOl8qCwp9NyDAcCDtq/QviLtYswW/VbWzUnTNE= github.com/ory/dockertest/v3 v3.6.5/go.mod h1:iYKQSRlYrt/2s5fJWYdB98kCQG6g/LjBMvzEYii63vg= -github.com/ory/dockertest/v3 v3.8.0/go.mod h1:9zPATATlWQru+ynXP+DytBQrsXV7Tmlx7K86H6fQaDo= github.com/ory/dockertest/v3 v3.8.1 h1:vU/8d1We4qIad2YM0kOwRVtnyue7ExvacPiw1yDm17g= github.com/ory/dockertest/v3 v3.8.1/go.mod h1:wSRQ3wmkz+uSARYMk7kVJFDBGm8x5gSxIhI7NDc+BAQ= github.com/ory/fosite v0.29.0/go.mod h1:0atSZmXO7CAcs6NPMI/Qtot8tmZYj04Nddoold4S2h0= @@ -1593,8 +1589,8 @@ github.com/ory/x v0.0.205/go.mod h1:A1s4iwmFIppRXZLF3J9GGWeY/HpREVm0Dk5z/787iek= github.com/ory/x v0.0.250/go.mod h1:jUJaVptu+geeqlb9SyQCogTKj5ztSDIF6APkhbKtwLc= github.com/ory/x v0.0.272/go.mod h1:1TTPgJGQutrhI2OnwdrTIHE9ITSf4MpzXFzA/ncTGRc= github.com/ory/x v0.0.288/go.mod h1:APpShLyJcVzKw1kTgrHI+j/L9YM+8BRjHlcYObc7C1U= -github.com/ory/x v0.0.330 h1:h+JhZb2DFBUbW5zebXmfdfZVPod+qyxm09ku5eFLciE= -github.com/ory/x v0.0.330/go.mod h1:VtcrHHCiLrKhxKUdtCNxQ9q/MILRnQmETNdH/jl9gKw= +github.com/ory/x v0.0.334 h1:ZtxDKRjrRYadZGYIg7kFI4wuEpRX7n5eMBQnxRU07lw= +github.com/ory/x v0.0.334/go.mod h1:vRr+//Cmpcu4HwkYwstv4mzie65ss+r76+iXU9fqQiA= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= diff --git a/persistence/sql/testhelpers/network.go b/persistence/sql/testhelpers/network.go index 935a9d260b31..70fa2419657a 100644 --- a/persistence/sql/testhelpers/network.go +++ b/persistence/sql/testhelpers/network.go @@ -2,11 +2,13 @@ package testhelpers import ( "context" + "testing" + db "github.com/gofrs/uuid" + courier "github.com/ory/kratos/courier/test" "github.com/ory/kratos/internal/testhelpers" "github.com/ory/kratos/persistence" - "testing" ) func DefaultNetworkWrapper(t *testing.T, ctx context.Context, p persistence.Persister) (courier.NetworkWrapper, courier.NetworkWrapper) { diff --git a/selfservice/hook/web_hook.go b/selfservice/hook/web_hook.go index b3b5da1b5093..6dd904430030 100644 --- a/selfservice/hook/web_hook.go +++ b/selfservice/hook/web_hook.go @@ -2,11 +2,14 @@ package hook import ( "bytes" + "context" "encoding/json" "fmt" "io" "net/http" + "github.com/hashicorp/go-retryablehttp" + "github.com/ory/x/fetcher" "github.com/ory/x/logrusx" @@ -30,33 +33,34 @@ var _ recovery.PostHookExecutor = new(WebHook) type ( AuthStrategy interface { - apply(req *http.Request) + apply(req *retryablehttp.Request) } authStrategyFactory func(c json.RawMessage) (AuthStrategy, error) - noopAuthStrategy struct{} + NoopAuthStrategy struct{} - basicAuthStrategy struct { + BasicAuthStrategy struct { user string password string } - apiKeyStrategy struct { + ApiKeyStrategy struct { name string value string in string } - webHookConfig struct { - method string - url string - templateURI string - auth AuthStrategy + WebHookConfig struct { + Method string + URL string + TemplateURI string + Auth AuthStrategy } webHookDependencies interface { x.LoggingProvider + x.HTTPClientProvider } templateContext struct { @@ -89,10 +93,10 @@ func newAuthStrategy(name string, c json.RawMessage) (as AuthStrategy, err error } func newNoopAuthStrategy(_ json.RawMessage) (AuthStrategy, error) { - return &noopAuthStrategy{}, nil + return &NoopAuthStrategy{}, nil } -func (c *noopAuthStrategy) apply(_ *http.Request) {} +func (c *NoopAuthStrategy) apply(_ *retryablehttp.Request) {} func newBasicAuthStrategy(raw json.RawMessage) (AuthStrategy, error) { type config struct { @@ -105,13 +109,13 @@ func newBasicAuthStrategy(raw json.RawMessage) (AuthStrategy, error) { return nil, err } - return &basicAuthStrategy{ + return &BasicAuthStrategy{ user: c.User, password: c.Password, }, nil } -func (c *basicAuthStrategy) apply(req *http.Request) { +func (c *BasicAuthStrategy) apply(req *retryablehttp.Request) { req.SetBasicAuth(c.user, c.password) } @@ -127,14 +131,14 @@ func newApiKeyStrategy(raw json.RawMessage) (AuthStrategy, error) { return nil, err } - return &apiKeyStrategy{ + return &ApiKeyStrategy{ in: c.In, name: c.Name, value: c.Value, }, nil } -func (c *apiKeyStrategy) apply(req *http.Request) { +func (c *ApiKeyStrategy) apply(req *retryablehttp.Request) { switch c.in { case "cookie": req.AddCookie(&http.Cookie{Name: c.name, Value: c.value}) @@ -143,7 +147,7 @@ func (c *apiKeyStrategy) apply(req *http.Request) { } } -func newWebHookConfig(r json.RawMessage) (*webHookConfig, error) { +func newWebHookConfig(r json.RawMessage) (*WebHookConfig, error) { type rawWebHookConfig struct { Method string Url string @@ -165,11 +169,11 @@ func newWebHookConfig(r json.RawMessage) (*webHookConfig, error) { return nil, fmt.Errorf("failed to create web hook auth strategy: %w", err) } - return &webHookConfig{ - method: rc.Method, - url: rc.Url, - templateURI: rc.Body, - auth: as, + return &WebHookConfig{ + Method: rc.Method, + URL: rc.Url, + TemplateURI: rc.Body, + Auth: as, }, nil } @@ -178,7 +182,7 @@ func NewWebHook(r webHookDependencies, c json.RawMessage) *WebHook { } func (e *WebHook) ExecuteLoginPreHook(_ http.ResponseWriter, req *http.Request, flow *login.Flow) error { - return e.execute(&templateContext{ + return e.execute(req.Context(), &templateContext{ Flow: flow, RequestHeaders: req.Header, RequestMethod: req.Method, @@ -187,7 +191,7 @@ func (e *WebHook) ExecuteLoginPreHook(_ http.ResponseWriter, req *http.Request, } func (e *WebHook) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, flow *login.Flow, session *session.Session) error { - return e.execute(&templateContext{ + return e.execute(req.Context(), &templateContext{ Flow: flow, RequestHeaders: req.Header, RequestMethod: req.Method, @@ -197,7 +201,7 @@ func (e *WebHook) ExecuteLoginPostHook(_ http.ResponseWriter, req *http.Request, } func (e *WebHook) ExecutePostVerificationHook(_ http.ResponseWriter, req *http.Request, flow *verification.Flow, identity *identity.Identity) error { - return e.execute(&templateContext{ + return e.execute(req.Context(), &templateContext{ Flow: flow, RequestHeaders: req.Header, RequestMethod: req.Method, @@ -207,7 +211,7 @@ func (e *WebHook) ExecutePostVerificationHook(_ http.ResponseWriter, req *http.R } func (e *WebHook) ExecutePostRecoveryHook(_ http.ResponseWriter, req *http.Request, flow *recovery.Flow, session *session.Session) error { - return e.execute(&templateContext{ + return e.execute(req.Context(), &templateContext{ Flow: flow, RequestHeaders: req.Header, RequestMethod: req.Method, @@ -217,7 +221,7 @@ func (e *WebHook) ExecutePostRecoveryHook(_ http.ResponseWriter, req *http.Reque } func (e *WebHook) ExecuteRegistrationPreHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow) error { - return e.execute(&templateContext{ + return e.execute(req.Context(), &templateContext{ Flow: flow, RequestHeaders: req.Header, RequestMethod: req.Method, @@ -226,7 +230,7 @@ func (e *WebHook) ExecuteRegistrationPreHook(_ http.ResponseWriter, req *http.Re } func (e *WebHook) ExecutePostRegistrationPostPersistHook(_ http.ResponseWriter, req *http.Request, flow *registration.Flow, session *session.Session) error { - return e.execute(&templateContext{ + return e.execute(req.Context(), &templateContext{ Flow: flow, RequestHeaders: req.Header, RequestMethod: req.Method, @@ -236,7 +240,7 @@ func (e *WebHook) ExecutePostRegistrationPostPersistHook(_ http.ResponseWriter, } func (e *WebHook) ExecuteSettingsPostPersistHook(_ http.ResponseWriter, req *http.Request, flow *settings.Flow, identity *identity.Identity) error { - return e.execute(&templateContext{ + return e.execute(req.Context(), &templateContext{ Flow: flow, RequestHeaders: req.Header, RequestMethod: req.Method, @@ -245,7 +249,7 @@ func (e *WebHook) ExecuteSettingsPostPersistHook(_ http.ResponseWriter, req *htt }) } -func (e *WebHook) execute(data *templateContext) error { +func (e *WebHook) execute(ctx context.Context, data *templateContext) error { // TODO: reminder for the future: move parsing of config to the web hook initialization conf, err := newWebHookConfig(e.c) if err != nil { @@ -253,11 +257,11 @@ func (e *WebHook) execute(data *templateContext) error { } var body io.Reader - if conf.method != "TRACE" { + if conf.Method != "TRACE" { // According to the HTTP spec any request method, but TRACE is allowed to // have a body. Even this is a really bad practice for some of them, like for // GET - body, err = createBody(e.r.Logger(), conf.templateURI, data) + body, err = createBody(e.r.Logger(), conf.TemplateURI, data) if err != nil { return fmt.Errorf("failed to create web hook body: %w", err) } @@ -266,7 +270,10 @@ func (e *WebHook) execute(data *templateContext) error { if body == nil { body = bytes.NewReader(make([]byte, 0)) } - if err = doHttpCall(conf.method, conf.url, conf.auth, body); err != nil { + + httpClient := e.r.HTTPClient(ctx) + + if err = doHttpCall(conf.Method, conf.URL, conf.Auth, body, httpClient); err != nil { return fmt.Errorf("failed to call web hook %w", err) } return nil @@ -310,8 +317,8 @@ func createBody(l *logrusx.Logger, templateURI string, data *templateContext) (* } } -func doHttpCall(method string, url string, as AuthStrategy, body io.Reader) error { - req, err := http.NewRequest(method, url, body) +func doHttpCall(method string, url string, as AuthStrategy, body io.Reader, hc *retryablehttp.Client) error { + req, err := retryablehttp.NewRequest(method, url, body) if err != nil { return err } @@ -319,7 +326,7 @@ func doHttpCall(method string, url string, as AuthStrategy, body io.Reader) erro as.apply(req) - resp, err := http.DefaultClient.Do(req) + resp, err := hc.Do(req) if err != nil { return err diff --git a/selfservice/hook/web_hook_integration_test.go b/selfservice/hook/web_hook_integration_test.go new file mode 100644 index 000000000000..1b8225738a90 --- /dev/null +++ b/selfservice/hook/web_hook_integration_test.go @@ -0,0 +1,394 @@ +package hook_test + +import ( + _ "embed" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/driver/config" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/hook" + + "github.com/ory/kratos/selfservice/flow/recovery" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/flow/settings" + "github.com/ory/kratos/selfservice/flow/verification" + + "github.com/ory/kratos/selfservice/flow" + + "github.com/julienschmidt/httprouter" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" + + "github.com/ory/kratos/session" + + "github.com/ory/kratos/selfservice/flow/login" + + "github.com/stretchr/testify/assert" +) + +func TestWebHooks(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + type WebHookRequest struct { + Body string + Headers http.Header + Method string + } + + webHookEndPoint := func(whr *WebHookRequest) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + whr.Body = string(body) + whr.Headers = r.Header + whr.Method = r.Method + } + } + + webHookHttpCodeEndPoint := func(code int) httprouter.Handle { + return func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { + w.WriteHeader(code) + } + } + + path := "/web_hook" + newServer := func(f httprouter.Handle) *httptest.Server { + r := httprouter.New() + + r.Handle("CONNECT", path, f) + r.DELETE(path, f) + r.GET(path, f) + r.OPTIONS(path, f) + r.PATCH(path, f) + r.POST(path, f) + r.PUT(path, f) + r.Handle("TRACE", path, f) + + ts := httptest.NewServer(r) + t.Cleanup(ts.Close) + return ts + } + + bodyWithFlowOnly := func(req *http.Request, f flow.Flow) string { + h, _ := json.Marshal(req.Header) + return fmt.Sprintf(`{ + "flow_id": "%s", + "identity_id": null, + "headers": %s, + "method": "%s", + "url": "%s" + }`, f.GetID(), string(h), req.Method, req.RequestURI) + } + + bodyWithFlowAndIdentity := func(req *http.Request, f flow.Flow, s *session.Session) string { + h, _ := json.Marshal(req.Header) + return fmt.Sprintf(`{ + "flow_id": "%s", + "identity_id": "%s", + "headers": %s, + "method": "%s", + "url": "%s" + }`, f.GetID(), s.Identity.ID, string(h), req.Method, req.RequestURI) + } + + for _, tc := range []struct { + uc string + callWebHook func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error + expectedBody func(req *http.Request, f flow.Flow, s *session.Session) string + createFlow func() flow.Flow + }{ + { + uc: "Pre 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.ExecuteLoginPreHook(nil, req, f.(*login.Flow)) + }, + expectedBody: func(req *http.Request, f flow.Flow, _ *session.Session) string { + return bodyWithFlowOnly(req, f) + }, + }, + { + uc: "Post Login Hook", + createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecuteLoginPostHook(nil, req, f.(*login.Flow), s) + }, + expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { + return bodyWithFlowAndIdentity(req, f, s) + }, + }, + { + uc: "Pre 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.ExecuteRegistrationPreHook(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 { return ®istration.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecutePostRegistrationPostPersistHook(nil, req, f.(*registration.Flow), s) + }, + expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { + return bodyWithFlowAndIdentity(req, f, s) + }, + }, + { + uc: "Post Recovery Hook", + createFlow: func() flow.Flow { return &recovery.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecutePostRecoveryHook(nil, req, f.(*recovery.Flow), s) + }, + expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { + return bodyWithFlowAndIdentity(req, f, s) + }, + }, + { + uc: "Post Verification Hook", + createFlow: func() flow.Flow { return &verification.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecutePostVerificationHook(nil, req, f.(*verification.Flow), s.Identity) + }, + expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { + return bodyWithFlowAndIdentity(req, f, s) + }, + }, + { + uc: "Post Settings Hook", + createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID()} }, + callWebHook: func(wh *hook.WebHook, req *http.Request, f flow.Flow, s *session.Session) error { + return wh.ExecuteSettingsPostPersistHook(nil, req, f.(*settings.Flow), s.Identity) + }, + expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { + return bodyWithFlowAndIdentity(req, f, s) + }, + }, + } { + t.Run("uc="+tc.uc, func(t *testing.T) { + for _, auth := range []struct { + uc string + createAuthConfig func() string + expectedHeader func(header http.Header) + }{ + { + uc: "no auth", + createAuthConfig: func() string { return "{}" }, + expectedHeader: func(header http.Header) {}, + }, + { + uc: "api key in header", + createAuthConfig: func() string { + return `{ + "type": "api_key", + "config": { + "name": "My-Key", + "value": "My-Key-Value", + "in": "header" + } + }` + }, + expectedHeader: func(header http.Header) { + header.Set("My-Key", "My-Key-Value") + }, + }, + { + uc: "api key in cookie", + createAuthConfig: func() string { + return `{ + "type": "api_key", + "config": { + "name": "My-Key", + "value": "My-Key-Value", + "in": "cookie" + } + }` + }, + expectedHeader: func(header http.Header) { + header.Set("Cookie", "My-Key=My-Key-Value") + }, + }, + { + uc: "basic auth", + createAuthConfig: func() string { + return `{ + "type": "basic_auth", + "config": { + "user": "My-User", + "password": "Super-Secret" + } + }` + }, + expectedHeader: func(header http.Header) { + header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("My-User:Super-Secret"))) + }, + }, + } { + t.Run("auth="+auth.uc, func(t *testing.T) { + for _, method := range []string{"CONNECT", "DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT", "TRACE", "GARBAGE"} { + t.Run("method="+method, func(t *testing.T) { + f := tc.createFlow() + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + RequestURI: "https://www.ory.sh/some_end_point", + Method: http.MethodPost, + } + s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}} + whr := &WebHookRequest{} + ts := newServer(webHookEndPoint(whr)) + conf := json.RawMessage(fmt.Sprintf(`{ + "url": "%s", + "method": "%s", + "body": "%s", + "auth": %s + }`, ts.URL+path, method, "./stub/test_body.jsonnet", auth.createAuthConfig())) + + wh := hook.NewWebHook(reg, conf) + + err := tc.callWebHook(wh, req, f, s) + if method == "GARBAGE" { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + + assert.Equal(t, method, whr.Method) + + expectedHeader := http.Header{} + expectedHeader.Set("Content-Type", "application/json") + auth.expectedHeader(expectedHeader) + for k, v := range expectedHeader { + vals := whr.Headers.Values(k) + assert.Equal(t, v, vals) + } + + if method != "TRACE" { + // According to the HTTP spec any request method, but TRACE is allowed to + // have a body. Even this is a really bad practice for some of them, like for + // GET + assert.JSONEq(t, tc.expectedBody(req, f, s), whr.Body) + } else { + assert.Emptyf(t, whr.Body, "HTTP %s is not allowed to have a body", method) + } + }) + } + }) + } + }) + } + + t.Run("Must error when config is erroneous", func(t *testing.T) { + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + RequestURI: "https://www.ory.sh/some_end_point", + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + conf := json.RawMessage("not valid json") + wh := hook.NewWebHook(reg, conf) + + err := wh.ExecuteLoginPreHook(nil, req, f) + assert.Error(t, err) + }) + + t.Run("Must error when template is erroneous", func(t *testing.T) { + ts := newServer(webHookHttpCodeEndPoint(200)) + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + RequestURI: "https://www.ory.sh/some_end_point", + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + conf := json.RawMessage(fmt.Sprintf(`{ + "url": "%s", + "method": "%s", + "body": "%s" + }`, ts.URL+path, "POST", "./stub/bad_template.jsonnet")) + wh := hook.NewWebHook(reg, conf) + + err := wh.ExecuteLoginPreHook(nil, req, f) + assert.Error(t, err) + }) + + boolToString := func(f bool) string { + if f { + return " not" + } else { + return "" + } + } + + for _, tc := range []struct { + code int + mustSuccess bool + }{ + {200, true}, + {299, true}, + {300, true}, + {399, true}, + {400, false}, + {499, false}, + {500, false}, + {599, false}, + } { + t.Run("Must"+boolToString(tc.mustSuccess)+" error when end point is returning "+strconv.Itoa(tc.code), func(t *testing.T) { + ts := newServer(webHookHttpCodeEndPoint(tc.code)) + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + RequestURI: "https://www.ory.sh/some_end_point", + Method: http.MethodPost, + } + f := &login.Flow{ID: x.NewUUID()} + conf := json.RawMessage(fmt.Sprintf(`{ + "url": "%s", + "method": "%s", + "body": "%s" + }`, ts.URL+path, "POST", "./stub/test_body.jsonnet")) + wh := hook.NewWebHook(reg, conf) + + err := wh.ExecuteLoginPreHook(nil, req, f) + if tc.mustSuccess { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestDisallowPrivateIPRanges(t *testing.T) { + conf, reg := internal.NewFastRegistryWithMocks(t) + conf.MustSet(config.ViperKeyClientHTTPNoPrivateIPRanges, true) + + req := &http.Request{ + Header: map[string][]string{"Some-Header": {"Some-Value"}}, + RequestURI: "https://www.ory.sh/some_end_point", + Method: http.MethodPost, + } + s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}} + f := &login.Flow{ID: x.NewUUID()} + wh := hook.NewWebHook(reg, json.RawMessage(`{ + "url": "https://localhost:1234/", + "method": "GET", + "body": "file://stub/test_body.jsonnet" +}`)) + err := wh.ExecuteLoginPostHook(nil, req, f, s) + require.Error(t, err) + require.Contains(t, err.Error(), "ip 127.0.0.1 is in the 127.0.0.0/8 range") +} diff --git a/selfservice/hook/web_hook_test.go b/selfservice/hook/web_hook_test.go index 27b3166fdda6..698f64b336bd 100644 --- a/selfservice/hook/web_hook_test.go +++ b/selfservice/hook/web_hook_test.go @@ -4,32 +4,19 @@ import ( _ "embed" "encoding/base64" "encoding/json" - "fmt" "io" - "io/ioutil" "net/http" - "net/http/httptest" - "strconv" "testing" + "github.com/hashicorp/go-retryablehttp" + "github.com/sirupsen/logrus/hooks/test" "github.com/ory/x/logrusx" - "github.com/ory/kratos/selfservice/flow/recovery" - "github.com/ory/kratos/selfservice/flow/registration" - "github.com/ory/kratos/selfservice/flow/settings" - "github.com/ory/kratos/selfservice/flow/verification" - - "github.com/ory/kratos/selfservice/flow" - - "github.com/julienschmidt/httprouter" - "github.com/ory/kratos/identity" "github.com/ory/kratos/x" - "github.com/ory/kratos/session" - "github.com/stretchr/testify/require" "github.com/ory/kratos/selfservice/flow/login" @@ -38,8 +25,8 @@ import ( ) func TestNoopAuthStrategy(t *testing.T) { - req := http.Request{Header: map[string][]string{}} - auth := noopAuthStrategy{} + req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}} + auth := NoopAuthStrategy{} auth.apply(&req) @@ -47,8 +34,8 @@ func TestNoopAuthStrategy(t *testing.T) { } func TestBasicAuthStrategy(t *testing.T) { - req := http.Request{Header: map[string][]string{}} - auth := basicAuthStrategy{ + req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}} + auth := BasicAuthStrategy{ user: "test-user", password: "test-pass", } @@ -63,8 +50,8 @@ func TestBasicAuthStrategy(t *testing.T) { } func TestApiKeyInHeaderStrategy(t *testing.T) { - req := http.Request{Header: map[string][]string{}} - auth := apiKeyStrategy{ + req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}} + auth := ApiKeyStrategy{ in: "header", name: "my-api-key-name", value: "my-api-key-value", @@ -79,8 +66,8 @@ func TestApiKeyInHeaderStrategy(t *testing.T) { } func TestApiKeyInCookieStrategy(t *testing.T) { - req := http.Request{Header: map[string][]string{}} - auth := apiKeyStrategy{ + req := retryablehttp.Request{Request: &http.Request{Header: map[string][]string{}}} + auth := ApiKeyStrategy{ in: "cookie", name: "my-api-key-name", value: "my-api-key-value", @@ -205,7 +192,7 @@ func TestWebHookConfig(t *testing.T) { "method": "POST", "body": "/path/to/my/jsonnet1.file" }`, - authStrategy: &noopAuthStrategy{}, + authStrategy: &NoopAuthStrategy{}, }, { strategy: "basic_auth", @@ -224,7 +211,7 @@ func TestWebHookConfig(t *testing.T) { } } }`, - authStrategy: &basicAuthStrategy{}, + authStrategy: &BasicAuthStrategy{}, }, { strategy: "api-key/header", @@ -244,7 +231,7 @@ func TestWebHookConfig(t *testing.T) { } } }`, - authStrategy: &apiKeyStrategy{}, + authStrategy: &ApiKeyStrategy{}, }, { strategy: "api-key/cookie", @@ -264,353 +251,18 @@ func TestWebHookConfig(t *testing.T) { } } }`, - authStrategy: &apiKeyStrategy{}, + authStrategy: &ApiKeyStrategy{}, }, } { t.Run("auth-strategy="+tc.strategy, func(t *testing.T) { conf, err := newWebHookConfig([]byte(tc.rawConfig)) assert.Nil(t, err) - assert.Equal(t, tc.url, conf.url) - assert.Equal(t, tc.method, conf.method) - assert.Equal(t, tc.body, conf.templateURI) - assert.NotNil(t, conf.auth) - assert.IsTypef(t, tc.authStrategy, conf.auth, "Auth should be of the expected type") - }) - } -} - -func TestWebHooks(t *testing.T) { - type WebHookRequest struct { - Body string - Headers http.Header - Method string - } - - webHookEndPoint := func(whr *WebHookRequest) httprouter.Handle { - return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - body, err := ioutil.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - whr.Body = string(body) - whr.Headers = r.Header - whr.Method = r.Method - } - } - - webHookHttpCodeEndPoint := func(code int) httprouter.Handle { - return func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { - w.WriteHeader(code) - } - } - - path := "/web_hook" - newServer := func(f httprouter.Handle) *httptest.Server { - r := httprouter.New() - - r.Handle("CONNECT", path, f) - r.DELETE(path, f) - r.GET(path, f) - r.OPTIONS(path, f) - r.PATCH(path, f) - r.POST(path, f) - r.PUT(path, f) - r.Handle("TRACE", path, f) - - ts := httptest.NewServer(r) - t.Cleanup(ts.Close) - return ts - } - - bodyWithFlowOnly := func(req *http.Request, f flow.Flow) string { - h, _ := json.Marshal(req.Header) - return fmt.Sprintf(`{ - "flow_id": "%s", - "identity_id": null, - "headers": %s, - "method": "%s", - "url": "%s" - }`, f.GetID(), string(h), req.Method, req.RequestURI) - } - - bodyWithFlowAndIdentity := func(req *http.Request, f flow.Flow, s *session.Session) string { - h, _ := json.Marshal(req.Header) - return fmt.Sprintf(`{ - "flow_id": "%s", - "identity_id": "%s", - "headers": %s, - "method": "%s", - "url": "%s" - }`, f.GetID(), s.Identity.ID, string(h), req.Method, req.RequestURI) - } - - for _, tc := range []struct { - uc string - callWebHook func(wh *WebHook, req *http.Request, f flow.Flow, s *session.Session) error - expectedBody func(req *http.Request, f flow.Flow, s *session.Session) string - createFlow func() flow.Flow - }{ - { - uc: "Pre Login Hook", - createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, - callWebHook: func(wh *WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { - return wh.ExecuteLoginPreHook(nil, req, f.(*login.Flow)) - }, - expectedBody: func(req *http.Request, f flow.Flow, _ *session.Session) string { - return bodyWithFlowOnly(req, f) - }, - }, - { - uc: "Post Login Hook", - createFlow: func() flow.Flow { return &login.Flow{ID: x.NewUUID()} }, - callWebHook: func(wh *WebHook, req *http.Request, f flow.Flow, s *session.Session) error { - return wh.ExecuteLoginPostHook(nil, req, f.(*login.Flow), s) - }, - expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) - }, - }, - { - uc: "Pre Registration Hook", - createFlow: func() flow.Flow { return ®istration.Flow{ID: x.NewUUID()} }, - callWebHook: func(wh *WebHook, req *http.Request, f flow.Flow, _ *session.Session) error { - return wh.ExecuteRegistrationPreHook(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 { return ®istration.Flow{ID: x.NewUUID()} }, - callWebHook: func(wh *WebHook, req *http.Request, f flow.Flow, s *session.Session) error { - return wh.ExecutePostRegistrationPostPersistHook(nil, req, f.(*registration.Flow), s) - }, - expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) - }, - }, - { - uc: "Post Recovery Hook", - createFlow: func() flow.Flow { return &recovery.Flow{ID: x.NewUUID()} }, - callWebHook: func(wh *WebHook, req *http.Request, f flow.Flow, s *session.Session) error { - return wh.ExecutePostRecoveryHook(nil, req, f.(*recovery.Flow), s) - }, - expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) - }, - }, - { - uc: "Post Verification Hook", - createFlow: func() flow.Flow { return &verification.Flow{ID: x.NewUUID()} }, - callWebHook: func(wh *WebHook, req *http.Request, f flow.Flow, s *session.Session) error { - return wh.ExecutePostVerificationHook(nil, req, f.(*verification.Flow), s.Identity) - }, - expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) - }, - }, - { - uc: "Post Settings Hook", - createFlow: func() flow.Flow { return &settings.Flow{ID: x.NewUUID()} }, - callWebHook: func(wh *WebHook, req *http.Request, f flow.Flow, s *session.Session) error { - return wh.ExecuteSettingsPostPersistHook(nil, req, f.(*settings.Flow), s.Identity) - }, - expectedBody: func(req *http.Request, f flow.Flow, s *session.Session) string { - return bodyWithFlowAndIdentity(req, f, s) - }, - }, - } { - t.Run("uc="+tc.uc, func(t *testing.T) { - for _, auth := range []struct { - uc string - createAuthConfig func() string - expectedHeader func(header http.Header) - }{ - { - uc: "no auth", - createAuthConfig: func() string { return "{}" }, - expectedHeader: func(header http.Header) {}, - }, - { - uc: "api key in header", - createAuthConfig: func() string { - return `{ - "type": "api_key", - "config": { - "name": "My-Key", - "value": "My-Key-Value", - "in": "header" - } - }` - }, - expectedHeader: func(header http.Header) { - header.Set("My-Key", "My-Key-Value") - }, - }, - { - uc: "api key in cookie", - createAuthConfig: func() string { - return `{ - "type": "api_key", - "config": { - "name": "My-Key", - "value": "My-Key-Value", - "in": "cookie" - } - }` - }, - expectedHeader: func(header http.Header) { - header.Set("Cookie", "My-Key=My-Key-Value") - }, - }, - { - uc: "basic auth", - createAuthConfig: func() string { - return `{ - "type": "basic_auth", - "config": { - "user": "My-User", - "password": "Super-Secret" - } - }` - }, - expectedHeader: func(header http.Header) { - header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("My-User:Super-Secret"))) - }, - }, - } { - t.Run("auth="+auth.uc, func(t *testing.T) { - for _, method := range []string{"CONNECT", "DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT", "TRACE", "GARBAGE"} { - t.Run("method="+method, func(t *testing.T) { - f := tc.createFlow() - req := &http.Request{ - Header: map[string][]string{"Some-Header": {"Some-Value"}}, - RequestURI: "https://www.ory.sh/some_end_point", - Method: http.MethodPost, - } - s := &session.Session{ID: x.NewUUID(), Identity: &identity.Identity{ID: x.NewUUID()}} - whr := &WebHookRequest{} - ts := newServer(webHookEndPoint(whr)) - conf := json.RawMessage(fmt.Sprintf(`{ - "url": "%s", - "method": "%s", - "body": "%s", - "auth": %s - }`, ts.URL+path, method, "./stub/test_body.jsonnet", auth.createAuthConfig())) - - wh := NewWebHook(&x.SimpleLogger{L: logrusx.New("kratos", "test")}, conf) - - err := tc.callWebHook(wh, req, f, s) - if method == "GARBAGE" { - assert.Error(t, err) - return - } - - assert.NoError(t, err) - - assert.Equal(t, method, whr.Method) - - expectedHeader := http.Header{} - expectedHeader.Set("Content-Type", "application/json") - auth.expectedHeader(expectedHeader) - for k, v := range expectedHeader { - vals := whr.Headers.Values(k) - assert.Equal(t, v, vals) - } - - if method != "TRACE" { - // According to the HTTP spec any request method, but TRACE is allowed to - // have a body. Even this is a really bad practice for some of them, like for - // GET - assert.JSONEq(t, tc.expectedBody(req, f, s), whr.Body) - } else { - assert.Emptyf(t, whr.Body, "HTTP %s is not allowed to have a body", method) - } - }) - } - }) - } - }) - } - - t.Run("Must error when config is erroneous", func(t *testing.T) { - req := &http.Request{ - Header: map[string][]string{"Some-Header": {"Some-Value"}}, - RequestURI: "https://www.ory.sh/some_end_point", - Method: http.MethodPost, - } - f := &login.Flow{ID: x.NewUUID()} - conf := json.RawMessage("not valid json") - wh := NewWebHook(&x.SimpleLogger{L: logrusx.New("kratos", "test")}, conf) - - err := wh.ExecuteLoginPreHook(nil, req, f) - assert.Error(t, err) - }) - - t.Run("Must error when template is erroneous", func(t *testing.T) { - ts := newServer(webHookHttpCodeEndPoint(200)) - req := &http.Request{ - Header: map[string][]string{"Some-Header": {"Some-Value"}}, - RequestURI: "https://www.ory.sh/some_end_point", - Method: http.MethodPost, - } - f := &login.Flow{ID: x.NewUUID()} - conf := json.RawMessage(fmt.Sprintf(`{ - "url": "%s", - "method": "%s", - "body": "%s" - }`, ts.URL+path, "POST", "./stub/bad_template.jsonnet")) - wh := NewWebHook(&x.SimpleLogger{L: logrusx.New("kratos", "test")}, conf) - - err := wh.ExecuteLoginPreHook(nil, req, f) - assert.Error(t, err) - }) - - boolToString := func(f bool) string { - if f { - return " not" - } else { - return "" - } - } - - for _, tc := range []struct { - code int - mustSuccess bool - }{ - {200, true}, - {299, true}, - {300, true}, - {399, true}, - {400, false}, - {499, false}, - {500, false}, - {599, false}, - } { - t.Run("Must"+boolToString(tc.mustSuccess)+" error when end point is returning "+strconv.Itoa(tc.code), func(t *testing.T) { - ts := newServer(webHookHttpCodeEndPoint(tc.code)) - req := &http.Request{ - Header: map[string][]string{"Some-Header": {"Some-Value"}}, - RequestURI: "https://www.ory.sh/some_end_point", - Method: http.MethodPost, - } - f := &login.Flow{ID: x.NewUUID()} - conf := json.RawMessage(fmt.Sprintf(`{ - "url": "%s", - "method": "%s", - "body": "%s" - }`, ts.URL+path, "POST", "./stub/test_body.jsonnet")) - wh := NewWebHook(&x.SimpleLogger{L: logrusx.New("kratos", "test")}, conf) - - err := wh.ExecuteLoginPreHook(nil, req, f) - if tc.mustSuccess { - assert.NoError(t, err) - } else { - assert.Error(t, err) - } + assert.Equal(t, tc.url, conf.URL) + assert.Equal(t, tc.method, conf.Method) + assert.Equal(t, tc.body, conf.TemplateURI) + assert.NotNil(t, conf.Auth) + assert.IsTypef(t, tc.authStrategy, conf.Auth, "Auth should be of the expected type") }) } } diff --git a/x/http.go b/x/http.go index 8a3829816ad7..c9ccf13627cf 100644 --- a/x/http.go +++ b/x/http.go @@ -1,6 +1,7 @@ package x import ( + "context" "io" "io/ioutil" "net/http" @@ -8,6 +9,8 @@ import ( "net/url" "testing" + "github.com/hashicorp/go-retryablehttp" + "github.com/golang/gddo/httputil" "github.com/ory/herodot" @@ -131,3 +134,7 @@ func AcceptsJSON(r *http.Request) bool { "application/json", }, "text/html") == "application/json" } + +type HTTPClientProvider interface { + HTTPClient(ctx context.Context) *retryablehttp.Client +}