diff --git a/docs/reference/filters.md b/docs/reference/filters.md index 98fea702f5..7b968367c0 100644 --- a/docs/reference/filters.md +++ b/docs/reference/filters.md @@ -1467,6 +1467,8 @@ jwtValidation("https://login.microsoftonline.com/{tenantId}/v2.0") The filter takes the header name as its first argument and sets header value to the token info or token introspection result serialized as a JSON object. +JSON object keys (as well keys of object values) are sorted therefore +header value is stable and can be used e.g. as a ratelimit key. To include only particular fields provide their names as additional arguments. If this filter is used when there is no token introspection or token info data diff --git a/filters/auth/forwardtoken.go b/filters/auth/forwardtoken.go index d46c38854e..ed930d004b 100644 --- a/filters/auth/forwardtoken.go +++ b/filters/auth/forwardtoken.go @@ -85,6 +85,7 @@ func (f *forwardTokenFilter) Request(ctx filters.FilterContext) { } } + // Produces payload with sorted keys payload, err := json.Marshal(tiMap) if err != nil { ctx.Logger().Errorf("Error while marshaling token: %v.", err) diff --git a/filters/auth/forwardtoken_test.go b/filters/auth/forwardtoken_test.go index 983f02f29d..eed64b655d 100644 --- a/filters/auth/forwardtoken_test.go +++ b/filters/auth/forwardtoken_test.go @@ -20,7 +20,7 @@ func staticServer(content string) *httptest.Server { } func TestForwardToken(t *testing.T) { - tokeninfoServer := staticServer(`{"uid": "test", "scope": ["uid"]}`) + tokeninfoServer := staticServer(`{"uid": "test", "scope": ["uid"], "obj": {"foo": "bar", "baz": "qux"}}`) defer tokeninfoServer.Close() introspectionServer := staticServer(`{"uid": "test-uid", "sub": "test-sub", "claims": {"email": "test@test.com"}, "active": true}`) @@ -38,7 +38,7 @@ func TestForwardToken(t *testing.T) { filters: `oauthTokeninfoAnyScope("uid") -> forwardToken("X-Skipper-Tokeninfo")`, header: http.Header{}, expectedHeader: http.Header{ - "X-Skipper-Tokeninfo": []string{`{"scope":["uid"],"uid":"test"}`}, + "X-Skipper-Tokeninfo": []string{`{"obj":{"baz":"qux","foo":"bar"},"scope":["uid"],"uid":"test"}`}, }, }, { @@ -49,10 +49,18 @@ func TestForwardToken(t *testing.T) { }, }, { - filters: `oauthTokeninfoAnyScope("uid") -> forwardToken("X-Skipper-Tokeninfo", "uid", "scope")`, + filters: `oauthTokeninfoAnyScope("uid") -> forwardToken("X-Skipper-Tokeninfo", "uid", "scope", "obj")`, header: http.Header{}, expectedHeader: http.Header{ - "X-Skipper-Tokeninfo": []string{`{"scope":["uid"],"uid":"test"}`}, + "X-Skipper-Tokeninfo": []string{`{"obj":{"baz":"qux","foo":"bar"},"scope":["uid"],"uid":"test"}`}, + }, + }, + { + // JSON value keys are sorted + filters: `oauthTokeninfoAnyScope("uid") -> forwardToken("X-Skipper-Tokeninfo", "obj", "scope", "uid")`, + header: http.Header{}, + expectedHeader: http.Header{ + "X-Skipper-Tokeninfo": []string{`{"obj":{"baz":"qux","foo":"bar"},"scope":["uid"],"uid":"test"}`}, }, }, { @@ -77,21 +85,24 @@ func TestForwardToken(t *testing.T) { }, }, { - filters: `forwardToken("X-Skipper-Tokeninfo")`, // not tokeninfo or tokenintrospection + // not tokeninfo or tokenintrospection + filters: `forwardToken("X-Skipper-Tokeninfo")`, header: http.Header{}, expectedHeader: http.Header{}, }, { - filters: `oauthTokeninfoAnyScope("uid") -> forwardToken("X-Skipper-Tokeninfo")`, // overwrites existing + // overwrites existing + filters: `oauthTokeninfoAnyScope("uid") -> forwardToken("X-Skipper-Tokeninfo")`, header: http.Header{ "X-Skipper-Tokeninfo": []string{`{"already": "exists"}`}, }, expectedHeader: http.Header{ - "X-Skipper-Tokeninfo": []string{`{"scope":["uid"],"uid":"test"}`}, + "X-Skipper-Tokeninfo": []string{`{"obj":{"baz":"qux","foo":"bar"},"scope":["uid"],"uid":"test"}`}, }, }, { - filters: `forwardToken("X-Skipper-Tokeninfo")`, // not tokeninfo or tokenintrospection, passes existing + // not tokeninfo or tokenintrospection, passes existing + filters: `forwardToken("X-Skipper-Tokeninfo")`, header: http.Header{ "X-Skipper-Tokeninfo": []string{`{"already": "exists"}`}, },