diff --git a/.schema/config.schema.json b/.schema/config.schema.json index 1c6faab5d3..21df163afc 100644 --- a/.schema/config.schema.json +++ b/.schema/config.schema.json @@ -430,6 +430,14 @@ "type": "boolean", "description": "When set to true the HTTP Header X-Forwarded-Host will be set to the original HTTP host." }, + "additional_headers": { + "title": "Set Additional HTTP Headers", + "type": "object", + "description": "Set additional HTTP Headers for the Session Check URL.", + "additionalProperties": { + "type": "string" + } + }, "extra_from": { "title": "Extra JSON Path", "description": "The `extra` field in the ORY Oathkeeper authentication session is set using this JSON Path. Defaults to `extra`, and could be `@this` (for the root element), `foo.bar` (for key foo.bar), or any other valid GJSON path. See [GSJON Syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for reference.", @@ -514,6 +522,12 @@ "type": "boolean", "description": "When set to true the HTTP Header X-Forwarded-Host will be set to the original HTTP host." }, + "additional_headers": { + "title": "Set Additional HTTP Headers", + "type": "object", + "description": "Set additional HTTP Headers for the Session Check URL.", + "additionalProperties": { "type": "string" } + }, "extra_from": { "title": "Extra JSON Path", "description": "The `extra` field in the ORY Oathkeeper authentication session is set using this JSON Path. Defaults to `extra`, and could be `@this` (for the root element), `foo.bar` (for key foo.bar), or any other valid GJSON path. See [GSJON Syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for reference.", diff --git a/go.mod b/go.mod index 82f272d8de..2453493303 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/aws/aws-sdk-go v1.31.13 github.com/blang/semver v3.5.1+incompatible github.com/bxcodec/faker v2.0.1+incompatible + github.com/davecgh/go-spew v1.1.1 github.com/dgraph-io/ristretto v0.0.2 github.com/dlclark/regexp2 v1.2.0 github.com/form3tech-oss/jwt-go v3.2.2+incompatible diff --git a/pipeline/authn/authenticator_bearer_token.go b/pipeline/authn/authenticator_bearer_token.go index 68aab862f4..35e1d8dfb5 100644 --- a/pipeline/authn/authenticator_bearer_token.go +++ b/pipeline/authn/authenticator_bearer_token.go @@ -30,6 +30,7 @@ type AuthenticatorBearerTokenConfiguration struct { PreserveHost bool `json:"preserve_host"` ExtraFrom string `json:"extra_from"` SubjectFrom string `json:"subject_from"` + SetHeaders map[string]string `json:"additional_headers"` } type AuthenticatorBearerToken struct { @@ -83,7 +84,7 @@ func (a *AuthenticatorBearerToken) Authenticate(r *http.Request, session *Authen return errors.WithStack(ErrAuthenticatorNotResponsible) } - body, err := forwardRequestToSessionStore(r, cf.CheckSessionURL, cf.PreservePath, cf.PreserveHost) + body, err := forwardRequestToSessionStore(r, cf.CheckSessionURL, cf.PreservePath, cf.PreserveHost, cf.SetHeaders) if err != nil { return err } diff --git a/pipeline/authn/authenticator_bearer_token_test.go b/pipeline/authn/authenticator_bearer_token_test.go index d41079d529..2eccc7417a 100644 --- a/pipeline/authn/authenticator_bearer_token_test.go +++ b/pipeline/authn/authenticator_bearer_token_test.go @@ -115,7 +115,7 @@ func TestAuthenticatorBearerToken(t *testing.T) { }, }, { - d: "should pass through method and headers ONLY to auth server when PreserveHost is true", + d: "should pass and set host when preserve_host is true", r: &http.Request{Host: "some-host", Header: http.Header{"Authorization": {"bearer zyx"}}, URL: &url.URL{Path: "/users/123?query=string"}, Method: "PUT"}, router: func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, r.Method, "PUT") @@ -130,6 +130,23 @@ func TestAuthenticatorBearerToken(t *testing.T) { Subject: "123", }, }, + { + d: "should pass and set additional hosts but not overwrite x-forwarded-host", + r: &http.Request{Host: "some-host", Header: http.Header{"Authorization": {"bearer zyx"}}, URL: &url.URL{Path: "/users/123?query=string"}, Method: "PUT"}, + router: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, r.Method, "PUT") + assert.Equal(t, "some-host", r.Header.Get("X-Forwarded-Host")) + assert.Equal(t, "bar", r.Header.Get("X-Foo")) + assert.Equal(t, r.Header.Get("Authorization"), "bearer zyx") + w.WriteHeader(200) + w.Write([]byte(`{"sub": "123"}`)) + }, + config: []byte(`{"preserve_host": true, "additional_headers": {"X-Foo": "bar","X-Forwarded-For": "not-some-host"}}`), + expectErr: false, + expectSess: &AuthenticationSession{ + Subject: "123", + }, + }, { d: "does not pass request body through to auth server", r: &http.Request{ diff --git a/pipeline/authn/authenticator_cookie_session.go b/pipeline/authn/authenticator_cookie_session.go index a0fa556552..0f1d2be1d8 100644 --- a/pipeline/authn/authenticator_cookie_session.go +++ b/pipeline/authn/authenticator_cookie_session.go @@ -28,12 +28,13 @@ type AuthenticatorCookieSessionFilter struct { } type AuthenticatorCookieSessionConfiguration struct { - Only []string `json:"only"` - CheckSessionURL string `json:"check_session_url"` - PreservePath bool `json:"preserve_path"` - ExtraFrom string `json:"extra_from"` - SubjectFrom string `json:"subject_from"` - PreserveHost bool `json:"preserve_host"` + Only []string `json:"only"` + CheckSessionURL string `json:"check_session_url"` + PreservePath bool `json:"preserve_path"` + ExtraFrom string `json:"extra_from"` + SubjectFrom string `json:"subject_from"` + PreserveHost bool `json:"preserve_host"` + SetHeaders map[string]string `json:"additional_headers"` } type AuthenticatorCookieSession struct { @@ -86,7 +87,7 @@ func (a *AuthenticatorCookieSession) Authenticate(r *http.Request, session *Auth return errors.WithStack(ErrAuthenticatorNotResponsible) } - body, err := forwardRequestToSessionStore(r, cf.CheckSessionURL, cf.PreservePath, cf.PreserveHost) + body, err := forwardRequestToSessionStore(r, cf.CheckSessionURL, cf.PreservePath, cf.PreserveHost, cf.SetHeaders) if err != nil { return err } @@ -126,7 +127,7 @@ func cookieSessionResponsible(r *http.Request, only []string) bool { return false } -func forwardRequestToSessionStore(r *http.Request, checkSessionURL string, preservePath bool, preserveHost bool) (json.RawMessage, error) { +func forwardRequestToSessionStore(r *http.Request, checkSessionURL string, preservePath bool, preserveHost bool, setHeaders map[string]string) (json.RawMessage, error) { reqUrl, err := url.Parse(checkSessionURL) if err != nil { return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to parse session check URL: %s", err)) @@ -142,6 +143,10 @@ func forwardRequestToSessionStore(r *http.Request, checkSessionURL string, prese Header: r.Header, } + for k, v := range setHeaders { + req.Header.Set(k, v) + } + if preserveHost { req.Header.Set("X-Forwarded-Host", r.Host) } diff --git a/pipeline/authn/authenticator_cookie_session_test.go b/pipeline/authn/authenticator_cookie_session_test.go index 8650d34fb9..c625dc609d 100644 --- a/pipeline/authn/authenticator_cookie_session_test.go +++ b/pipeline/authn/authenticator_cookie_session_test.go @@ -109,6 +109,27 @@ func TestAuthenticatorCookieSession(t *testing.T) { assert.Equal(t, &AuthenticationSession{Subject: "123"}, session) }) + t.Run("description=should pass not override x-forwarded-host in set_headers", func(t *testing.T) { + testServer, requestRecorder := makeServer(200, `{"subject": "123"}`) + req := makeRequest("PUT", "/users/123?query=string", map[string]string{"sessionid": "zyx"}, "") + expectedHost := "some-host" + req.Host = expectedHost + err := pipelineAuthenticator.Authenticate( + req, + session, + json.RawMessage(fmt.Sprintf(`{"check_session_url": "%s", "additional_headers": {"X-Forwarded-Host": "not-some-host", "X-Foo": "bar"}, "preserve_host": true}`, testServer.URL)), + nil, + ) + require.NoError(t, err, "%#v", errors.Cause(err)) + assert.Len(t, requestRecorder.requests, 1) + r := requestRecorder.requests[0] + assert.Equal(t, r.Method, "PUT") + assert.Equal(t, expectedHost, r.Header.Get("X-Forwarded-Host")) + assert.Equal(t, "bar", r.Header.Get("X-Foo"), "%+v", r.Header) + assert.Equal(t, r.Header.Get("Cookie"), "sessionid=zyx") + assert.Equal(t, &AuthenticationSession{Subject: "123"}, session) + }) + t.Run("description=does not pass request body through to auth server", func(t *testing.T) { testServer, requestRecorder := makeServer(200, `{}`) pipelineAuthenticator.Authenticate(