From 886997e6b890b8023834a197ff3307f517e548f4 Mon Sep 17 00:00:00 2001 From: "Huabing (Robin) Zhao" Date: Wed, 29 Jul 2026 01:00:27 -0700 Subject: [PATCH 1/2] build(deps): bump go-control-plane for oauth2 post_logout_redirect_uri Picks up envoyproxy/envoy#45367, which adds the post_logout_redirect_uri field to the OAuth2 filter config. Also regenerates extensions.gen.go for the extension types added since the previous pin. Signed-off-by: Huabing (Robin) Zhao --- examples/extension-server/go.mod | 8 +++---- examples/extension-server/go.sum | 20 ++++++++-------- go.mod | 14 ++++++------ go.sum | 28 +++++++++++------------ internal/xds/extensions/extensions.gen.go | 6 +++++ test/go.mod | 14 ++++++------ test/go.sum | 28 +++++++++++------------ 7 files changed, 62 insertions(+), 56 deletions(-) diff --git a/examples/extension-server/go.mod b/examples/extension-server/go.mod index 9a7aa5196f..4ba5aded4d 100644 --- a/examples/extension-server/go.mod +++ b/examples/extension-server/go.mod @@ -4,8 +4,8 @@ go 1.26.5 require ( github.com/envoyproxy/gateway v1.3.1 - github.com/envoyproxy/go-control-plane v0.14.1-0.20260627225610-70ff85c381ff - github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260627225610-70ff85c381ff + github.com/envoyproxy/go-control-plane v0.14.1-0.20260729034317-78e59f151126 + github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260729034317-78e59f151126 github.com/urfave/cli/v2 v2.27.7 google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af @@ -59,8 +59,8 @@ require ( golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.40.0 // indirect golang.org/x/tools v0.47.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/examples/extension-server/go.sum b/examples/extension-server/go.sum index 6d5b92446e..78b09a41bb 100644 --- a/examples/extension-server/go.sum +++ b/examples/extension-server/go.sum @@ -21,10 +21,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.14.1-0.20260627225610-70ff85c381ff h1:tDxhbFOO5qR1vgixjuOI5RBxASCvac4Ki3EgC4dBDPI= -github.com/envoyproxy/go-control-plane v0.14.1-0.20260627225610-70ff85c381ff/go.mod h1:H3lDamtuGa0Y80VmchcmbutXMYNPDr0j+JrLlsgyEyo= -github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260627225610-70ff85c381ff h1:stwP9x94QfAFs+RF+YFkSrSuTxBuVrj6Sv+PJXJkXzo= -github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260627225610-70ff85c381ff/go.mod h1:RgJXVdNtBhId0AeGnDEqPRSejRMoz//JumYvSTcJTvM= +github.com/envoyproxy/go-control-plane v0.14.1-0.20260729034317-78e59f151126 h1:YBp3WmY3V1e8QDWIRgGbtCRWoaVzMV84pcj4Ji1sGOc= +github.com/envoyproxy/go-control-plane v0.14.1-0.20260729034317-78e59f151126/go.mod h1:rcDQvjX9fwNQvh7l9pxwfGBufBWU6xQ0zK5mcsH8/Lg= +github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260729034317-78e59f151126 h1:Q2dTxj798lXRQdDUf89aMqzRc+JFKBuRugTgQJSA7Nc= +github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260729034317-78e59f151126/go.mod h1:rYoRI6N8FcjZVmmeKqH8/ykAhfro9Bg8+of2IpBseyQ= github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -177,8 +177,8 @@ go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRk go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -209,10 +209,10 @@ golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTF golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 h1:admdQBe8jR3VWhBsUrAOaF2Qw6K/+p5pSm1GN8+6Fw4= -google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800/go.mod h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a h1:97PfJ4tCxY5C7NzzgGqQEMZmXbISdvSArNNEOoUGKBg= +google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a h1:qI/YMH1ep2qQtqcp00gMQyoU7mjvbhg88GJKCvfoLj0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= diff --git a/go.mod b/go.mod index 588892b64b..3d919562ca 100644 --- a/go.mod +++ b/go.mod @@ -12,10 +12,10 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/docker/cli v29.6.2+incompatible github.com/dominikbraun/graph v0.23.0 - github.com/envoyproxy/go-control-plane v0.14.1-0.20260627225610-70ff85c381ff - github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260627225610-70ff85c381ff - github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260627225610-70ff85c381ff - github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260627225610-70ff85c381ff + github.com/envoyproxy/go-control-plane v0.14.1-0.20260729034317-78e59f151126 + github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260729034317-78e59f151126 + github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260729034317-78e59f151126 + github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260729034317-78e59f151126 github.com/envoyproxy/ratelimit v1.4.1-0.20260122083618-3fb702589d36 github.com/evanphx/json-patch v5.9.11+incompatible github.com/evanphx/json-patch/v5 v5.9.11 @@ -53,11 +53,11 @@ require ( go.opentelemetry.io/otel/sdk v1.44.0 go.opentelemetry.io/otel/sdk/metric v1.44.0 go.opentelemetry.io/otel/trace v1.44.0 - go.opentelemetry.io/proto/otlp v1.10.0 + go.opentelemetry.io/proto/otlp v1.11.0 go.uber.org/zap v1.28.0 golang.org/x/sync v0.22.0 gomodules.xyz/jsonpatch/v2 v2.5.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a google.golang.org/grpc v1.82.1 google.golang.org/grpc/security/advancedtls v1.0.0 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af @@ -267,7 +267,7 @@ require ( golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.47.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 564c928a7e..f7c0dffdcb 100644 --- a/go.sum +++ b/go.sum @@ -147,14 +147,14 @@ github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/ github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.14.1-0.20260627225610-70ff85c381ff h1:tDxhbFOO5qR1vgixjuOI5RBxASCvac4Ki3EgC4dBDPI= -github.com/envoyproxy/go-control-plane v0.14.1-0.20260627225610-70ff85c381ff/go.mod h1:H3lDamtuGa0Y80VmchcmbutXMYNPDr0j+JrLlsgyEyo= -github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260627225610-70ff85c381ff h1:uPvDnxeo1yGkZS0DKq28GprxMhfNZogPTi0+u6K8Kd0= -github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260627225610-70ff85c381ff/go.mod h1:v21y1Uq30hmHbNsT2JOGMJG9cF+Ls7zTfIVOBLAU1TE= -github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260627225610-70ff85c381ff h1:stwP9x94QfAFs+RF+YFkSrSuTxBuVrj6Sv+PJXJkXzo= -github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260627225610-70ff85c381ff/go.mod h1:RgJXVdNtBhId0AeGnDEqPRSejRMoz//JumYvSTcJTvM= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260627225610-70ff85c381ff h1:Co6W1/F/z5JehSpnO4GLI8xkeJzO851Y5fQ7Bfxc48M= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260627225610-70ff85c381ff/go.mod h1:YySqCcozu0HwklKZzeX6N98q+TyqEkgX2sg7DQqiJfU= +github.com/envoyproxy/go-control-plane v0.14.1-0.20260729034317-78e59f151126 h1:YBp3WmY3V1e8QDWIRgGbtCRWoaVzMV84pcj4Ji1sGOc= +github.com/envoyproxy/go-control-plane v0.14.1-0.20260729034317-78e59f151126/go.mod h1:rcDQvjX9fwNQvh7l9pxwfGBufBWU6xQ0zK5mcsH8/Lg= +github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260729034317-78e59f151126 h1:HrAd+uyPv4ns7TR7EftAEI0wMoHlTV1USXW/x9adXpk= +github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260729034317-78e59f151126/go.mod h1:pdjA+146jsWRsJ0M1jgfRrvJq1HPWeyvaT/Va+nV2OY= +github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260729034317-78e59f151126 h1:Q2dTxj798lXRQdDUf89aMqzRc+JFKBuRugTgQJSA7Nc= +github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260729034317-78e59f151126/go.mod h1:rYoRI6N8FcjZVmmeKqH8/ykAhfro9Bg8+of2IpBseyQ= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260729034317-78e59f151126 h1:R2YGwjxI7IiCpcJqsapTrw5XwMYWjmek6drfv/wKS6c= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260729034317-78e59f151126/go.mod h1:kFrXkz+a+gF/F56IcDXtBE0QfpZfFTyf92okqDzIBu0= github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/envoyproxy/ratelimit v1.4.1-0.20260122083618-3fb702589d36 h1:nEi1OH2qhE8NtcuBgO/uKpTw/P0nVu4i8mZvL6oD9CQ= @@ -643,8 +643,8 @@ go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRk go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -728,10 +728,10 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 h1:admdQBe8jR3VWhBsUrAOaF2Qw6K/+p5pSm1GN8+6Fw4= -google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800/go.mod h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a h1:97PfJ4tCxY5C7NzzgGqQEMZmXbISdvSArNNEOoUGKBg= +google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a h1:qI/YMH1ep2qQtqcp00gMQyoU7mjvbhg88GJKCvfoLj0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds= diff --git a/internal/xds/extensions/extensions.gen.go b/internal/xds/extensions/extensions.gen.go index 921699699e..e85f24a833 100644 --- a/internal/xds/extensions/extensions.gen.go +++ b/internal/xds/extensions/extensions.gen.go @@ -121,6 +121,7 @@ import ( _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/a2a/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/adaptive_concurrency/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/admission_control/v3" + _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/ai_protocol_manager/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/alternate_protocols_cache/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/api_key_auth/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/aws_lambda/v3" @@ -233,6 +234,7 @@ import ( _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/dns_filter/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/dynamic_modules/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/udp_proxy/session/dynamic_forward_proxy/v3" + _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/udp_proxy/session/ext_authz/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/udp_proxy/session/http_capsule/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/udp_proxy/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/formatter/cel/v3" @@ -256,6 +258,7 @@ import ( _ "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/tls/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/xds/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/health_check/event_sinks/file/v3" + _ "github.com/envoyproxy/go-control-plane/envoy/extensions/health_checkers/dynamic_modules/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/health_checkers/redis/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/health_checkers/thrift/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/http/cache/file_system_http_cache/v3" @@ -312,6 +315,7 @@ import ( _ "github.com/envoyproxy/go-control-plane/envoy/extensions/network/dns_resolver/cares/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/network/dns_resolver/getaddrinfo/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/network/dns_resolver/hickory/v3" + _ "github.com/envoyproxy/go-control-plane/envoy/extensions/network/socket_interface/sockmap/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/network/socket_interface/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/outlier_detection_monitors/common/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/outlier_detection_monitors/consecutive_errors/v3" @@ -372,12 +376,14 @@ import ( _ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/dynamic_modules/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/generic/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/http/v3" + _ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/reverse_tunnel/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/tcp/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/udp/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/tcp/generic/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/tcp/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3" + _ "github.com/envoyproxy/go-control-plane/envoy/extensions/watchdog/backtrace_action/v3" _ "github.com/envoyproxy/go-control-plane/envoy/extensions/watchdog/profile_action/v3" _ "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v3" _ "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" diff --git a/test/go.mod b/test/go.mod index 1174f27b0d..b65ab7afbc 100644 --- a/test/go.mod +++ b/test/go.mod @@ -21,7 +21,7 @@ require ( github.com/prometheus/common v0.70.0 github.com/quic-go/quic-go v0.59.1 github.com/stretchr/testify v1.11.1 - go.opentelemetry.io/proto/otlp v1.10.0 + go.opentelemetry.io/proto/otlp v1.11.0 golang.org/x/net v0.57.0 gonum.org/v1/gonum v0.17.0 google.golang.org/grpc v1.82.1 @@ -106,10 +106,10 @@ require ( github.com/dominikbraun/graph v0.23.0 // indirect github.com/ebitengine/purego v0.10.1 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect - github.com/envoyproxy/go-control-plane v0.14.1-0.20260627225610-70ff85c381ff // indirect - github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260627225610-70ff85c381ff // indirect - github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260627225610-70ff85c381ff // indirect - github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260627225610-70ff85c381ff // indirect + github.com/envoyproxy/go-control-plane v0.14.1-0.20260729034317-78e59f151126 // indirect + github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260729034317-78e59f151126 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260729034317-78e59f151126 // indirect + github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260729034317-78e59f151126 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/envoyproxy/ratelimit v1.4.1-0.20260122083618-3fb702589d36 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect @@ -279,8 +279,8 @@ require ( golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.47.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/test/go.sum b/test/go.sum index 0fe34f2e94..d8415982d9 100644 --- a/test/go.sum +++ b/test/go.sum @@ -177,14 +177,14 @@ github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/ github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.14.1-0.20260627225610-70ff85c381ff h1:tDxhbFOO5qR1vgixjuOI5RBxASCvac4Ki3EgC4dBDPI= -github.com/envoyproxy/go-control-plane v0.14.1-0.20260627225610-70ff85c381ff/go.mod h1:H3lDamtuGa0Y80VmchcmbutXMYNPDr0j+JrLlsgyEyo= -github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260627225610-70ff85c381ff h1:uPvDnxeo1yGkZS0DKq28GprxMhfNZogPTi0+u6K8Kd0= -github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260627225610-70ff85c381ff/go.mod h1:v21y1Uq30hmHbNsT2JOGMJG9cF+Ls7zTfIVOBLAU1TE= -github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260627225610-70ff85c381ff h1:stwP9x94QfAFs+RF+YFkSrSuTxBuVrj6Sv+PJXJkXzo= -github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260627225610-70ff85c381ff/go.mod h1:RgJXVdNtBhId0AeGnDEqPRSejRMoz//JumYvSTcJTvM= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260627225610-70ff85c381ff h1:Co6W1/F/z5JehSpnO4GLI8xkeJzO851Y5fQ7Bfxc48M= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260627225610-70ff85c381ff/go.mod h1:YySqCcozu0HwklKZzeX6N98q+TyqEkgX2sg7DQqiJfU= +github.com/envoyproxy/go-control-plane v0.14.1-0.20260729034317-78e59f151126 h1:YBp3WmY3V1e8QDWIRgGbtCRWoaVzMV84pcj4Ji1sGOc= +github.com/envoyproxy/go-control-plane v0.14.1-0.20260729034317-78e59f151126/go.mod h1:rcDQvjX9fwNQvh7l9pxwfGBufBWU6xQ0zK5mcsH8/Lg= +github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260729034317-78e59f151126 h1:HrAd+uyPv4ns7TR7EftAEI0wMoHlTV1USXW/x9adXpk= +github.com/envoyproxy/go-control-plane/contrib v1.36.1-0.20260729034317-78e59f151126/go.mod h1:pdjA+146jsWRsJ0M1jgfRrvJq1HPWeyvaT/Va+nV2OY= +github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260729034317-78e59f151126 h1:Q2dTxj798lXRQdDUf89aMqzRc+JFKBuRugTgQJSA7Nc= +github.com/envoyproxy/go-control-plane/envoy v1.37.1-0.20260729034317-78e59f151126/go.mod h1:rYoRI6N8FcjZVmmeKqH8/ykAhfro9Bg8+of2IpBseyQ= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260729034317-78e59f151126 h1:R2YGwjxI7IiCpcJqsapTrw5XwMYWjmek6drfv/wKS6c= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.1-0.20260729034317-78e59f151126/go.mod h1:kFrXkz+a+gF/F56IcDXtBE0QfpZfFTyf92okqDzIBu0= github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/envoyproxy/ratelimit v1.4.1-0.20260122083618-3fb702589d36 h1:nEi1OH2qhE8NtcuBgO/uKpTw/P0nVu4i8mZvL6oD9CQ= @@ -669,8 +669,8 @@ go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRk go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= -go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= @@ -752,10 +752,10 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 h1:admdQBe8jR3VWhBsUrAOaF2Qw6K/+p5pSm1GN8+6Fw4= -google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800/go.mod h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a h1:97PfJ4tCxY5C7NzzgGqQEMZmXbISdvSArNNEOoUGKBg= +google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a h1:qI/YMH1ep2qQtqcp00gMQyoU7mjvbhg88GJKCvfoLj0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/grpc/security/advancedtls v1.0.0 h1:/KQ7VP/1bs53/aopk9QhuPyFAp9Dm9Ejix3lzYkCrDA= From 4eac00117b90203da9f220b158adf6b666bf6fa6 Mon Sep 17 00:00:00 2001 From: "Huabing (Robin) Zhao" Date: Wed, 29 Jul 2026 01:58:20 -0700 Subject: [PATCH 2/2] feat(oidc): support post_logout_redirect_uri in logout Adds spec.oidc.postLogoutRedirect to SecurityPolicy, controlling the post_logout_redirect_uri parameter Envoy sends to the OIDC provider's end session endpoint during RP-Initiated Logout. Set uri to send a specific value, or disabled to omit the parameter entirely. Envoy previously hardcoded this parameter to :///, the root of the inbound request's host. Many providers require the post logout redirect URI to be registered for the client and reject the logout request otherwise, so that default made RP-Initiated Logout unusable for them. The uri accepts the %REQ(header)% command operator so one policy can serve several hosts. Other operators are rejected up front, because Envoy fails filter creation on an unknown operator and would NACK the xDS update rather than report the mistake on the policy. The uri/disabled exclusivity is re-checked after policy merge as well, since CEL only ever sees an individual policy and a StrategicMerge can leave both fields set. Fixes #7349 Signed-off-by: Huabing (Robin) Zhao --- api/v1alpha1/oidc_types.go | 52 +++++ api/v1alpha1/zz_generated.deepcopy.go | 30 +++ ...ateway.envoyproxy.io_securitypolicies.yaml | 45 ++++ ...ateway.envoyproxy.io_securitypolicies.yaml | 45 ++++ internal/gatewayapi/securitypolicy.go | 117 ++++++++++ internal/gatewayapi/securitypolicy_test.go | 209 ++++++++++++++++++ .../testdata/securitypolicy-with-oidc.in.yaml | 6 + .../securitypolicy-with-oidc.out.yaml | 12 + internal/ir/xds.go | 4 + internal/ir/zz_generated.deepcopy.go | 5 + internal/xds/translator/oidc.go | 18 ++ .../translator/testdata/in/xds-ir/oidc.yaml | 8 + .../testdata/out/xds-ir/oidc.routes.yaml | 6 + .../7349-oidc-post-logout-redirect-uri.md | 1 + site/content/en/latest/api/extension_types.md | 19 ++ site/content/en/latest/tasks/security/oidc.md | 78 +++++++ test/cel-validation/securitypolicy_test.go | 207 +++++++++++++++++ test/e2e/testdata/oidc-securitypolicy.yaml | 4 + test/e2e/tests/oidc.go | 39 +++- test/helm/gateway-crds-helm/all.out.yaml | 45 ++++ test/helm/gateway-crds-helm/e2e.out.yaml | 45 ++++ .../envoy-gateway-crds.out.yaml | 45 ++++ 22 files changed, 1034 insertions(+), 6 deletions(-) create mode 100644 release-notes/current/new_features/7349-oidc-post-logout-redirect-uri.md diff --git a/api/v1alpha1/oidc_types.go b/api/v1alpha1/oidc_types.go index 910813feae..9cad427ad8 100644 --- a/api/v1alpha1/oidc_types.go +++ b/api/v1alpha1/oidc_types.go @@ -94,6 +94,20 @@ type OIDC struct { // If not specified, uses a default logout path "/logout" LogoutPath *string `json:"logoutPath,omitempty"` + // PostLogoutRedirect configures the `post_logout_redirect_uri` parameter that EG sends to the + // OIDC Provider's end session endpoint when a user accesses the logout path. + // + // This only applies when the OIDC Provider's end session endpoint is configured or discovered, + // i.e. when RP-Initiated Logout is in use. It is ignored otherwise. + // + // If not specified, Envoy sends the root of the request's host, ":///". + // Note that many OIDC Providers require the post logout redirect URI to be pre-registered for + // the client and reject the logout request otherwise. If the default value is not registered, + // set an explicit uri here, or set disabled to true to omit the parameter altogether. + // + // +optional + PostLogoutRedirect *OIDCPostLogoutRedirect `json:"postLogoutRedirect,omitempty"` + // ForwardAccessToken indicates whether the Envoy should forward the access token // via the Authorization header Bearer scheme to the upstream. // If not specified, defaults to false. @@ -235,6 +249,44 @@ type OIDCDenyRedirectHeader struct { StringMatch `json:",inline"` } +// OIDCPostLogoutRedirect configures the `post_logout_redirect_uri` parameter used in OIDC +// [RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) requests. +// +// Exactly one of uri or disabled must be set. +// +// +kubebuilder:validation:XValidation:rule="has(self.uri) != has(self.disabled)",message="exactly one of uri or disabled must be set" +type OIDCPostLogoutRedirect struct { + // URI is sent as the `post_logout_redirect_uri` parameter to the OIDC Provider's end session + // endpoint. The provider redirects the user to this URI after the logout completes, so it + // usually must be pre-registered for the client with the provider. + // + // The URI may contain the Envoy "%REQ(header)%" + // [command operator](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) + // to build the URI from the request, for example + // "%REQ(x-forwarded-proto)%://%REQ(:authority)%/loggedout". Envoy Gateway accepts only that + // operator here, since it is the only one meaningful in a URI derived from the request, and + // rejects any other so that a typo surfaces on this policy instead of being rejected by Envoy + // as an invalid configuration. A literal percent is written as "%%". + // + // The scheme must be http, https, or a "%REQ(header)%" command operator. The URI is + // percent-encoded automatically when the logout URL is built, so do not pre-encode it. + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=2048 + URI *string `json:"uri,omitempty"` + + // Disabled omits the `post_logout_redirect_uri` parameter from the logout request entirely. + // Use this when the OIDC Provider rejects unregistered post logout redirect URIs and you do + // not need the user redirected back after the logout completes. In that case the user is left + // on a page controlled by the provider. + // + // Setting this to false is equivalent to leaving postLogoutRedirect unset. + // + // +optional + Disabled *bool `json:"disabled,omitempty"` +} + // OIDCCookieNames defines the names of cookies to use in the Envoy OIDC filter. type OIDCCookieNames struct { // The name of the cookie used to store the AccessToken in the diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 9c32ace0fa..6d128a7aab 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -6076,6 +6076,11 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { *out = new(string) **out = **in } + if in.PostLogoutRedirect != nil { + in, out := &in.PostLogoutRedirect, &out.PostLogoutRedirect + *out = new(OIDCPostLogoutRedirect) + (*in).DeepCopyInto(*out) + } if in.ForwardAccessToken != nil { in, out := &in.ForwardAccessToken, &out.ForwardAccessToken *out = new(bool) @@ -6211,6 +6216,31 @@ func (in *OIDCDenyRedirectHeader) DeepCopy() *OIDCDenyRedirectHeader { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCPostLogoutRedirect) DeepCopyInto(out *OIDCPostLogoutRedirect) { + *out = *in + if in.URI != nil { + in, out := &in.URI, &out.URI + *out = new(string) + **out = **in + } + if in.Disabled != nil { + in, out := &in.Disabled, &out.Disabled + *out = new(bool) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCPostLogoutRedirect. +func (in *OIDCPostLogoutRedirect) DeepCopy() *OIDCPostLogoutRedirect { + if in == nil { + return nil + } + out := new(OIDCPostLogoutRedirect) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCProvider) DeepCopyInto(out *OIDCProvider) { *out = *in diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml index 70bbc2523a..0c83359685 100644 --- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -5768,6 +5768,51 @@ spec: If not specified, defaults to false. type: boolean + postLogoutRedirect: + description: |- + PostLogoutRedirect configures the `post_logout_redirect_uri` parameter that EG sends to the + OIDC Provider's end session endpoint when a user accesses the logout path. + + This only applies when the OIDC Provider's end session endpoint is configured or discovered, + i.e. when RP-Initiated Logout is in use. It is ignored otherwise. + + If not specified, Envoy sends the root of the request's host, ":///". + Note that many OIDC Providers require the post logout redirect URI to be pre-registered for + the client and reject the logout request otherwise. If the default value is not registered, + set an explicit uri here, or set disabled to true to omit the parameter altogether. + properties: + disabled: + description: |- + Disabled omits the `post_logout_redirect_uri` parameter from the logout request entirely. + Use this when the OIDC Provider rejects unregistered post logout redirect URIs and you do + not need the user redirected back after the logout completes. In that case the user is left + on a page controlled by the provider. + + Setting this to false is equivalent to leaving postLogoutRedirect unset. + type: boolean + uri: + description: |- + URI is sent as the `post_logout_redirect_uri` parameter to the OIDC Provider's end session + endpoint. The provider redirects the user to this URI after the logout completes, so it + usually must be pre-registered for the client with the provider. + + The URI may contain the Envoy "%REQ(header)%" + [command operator](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) + to build the URI from the request, for example + "%REQ(x-forwarded-proto)%://%REQ(:authority)%/loggedout". Envoy Gateway accepts only that + operator here, since it is the only one meaningful in a URI derived from the request, and + rejects any other so that a typo surfaces on this policy instead of being rejected by Envoy + as an invalid configuration. A literal percent is written as "%%". + + The scheme must be http, https, or a "%REQ(header)%" command operator. The URI is + percent-encoded automatically when the logout URL is built, so do not pre-encode it. + maxLength: 2048 + minLength: 1 + type: string + type: object + x-kubernetes-validations: + - message: exactly one of uri or disabled must be set + rule: has(self.uri) != has(self.disabled) provider: description: The OIDC Provider configuration. properties: diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml index 1fa6777cae..8536c1d3d2 100644 --- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -5767,6 +5767,51 @@ spec: If not specified, defaults to false. type: boolean + postLogoutRedirect: + description: |- + PostLogoutRedirect configures the `post_logout_redirect_uri` parameter that EG sends to the + OIDC Provider's end session endpoint when a user accesses the logout path. + + This only applies when the OIDC Provider's end session endpoint is configured or discovered, + i.e. when RP-Initiated Logout is in use. It is ignored otherwise. + + If not specified, Envoy sends the root of the request's host, ":///". + Note that many OIDC Providers require the post logout redirect URI to be pre-registered for + the client and reject the logout request otherwise. If the default value is not registered, + set an explicit uri here, or set disabled to true to omit the parameter altogether. + properties: + disabled: + description: |- + Disabled omits the `post_logout_redirect_uri` parameter from the logout request entirely. + Use this when the OIDC Provider rejects unregistered post logout redirect URIs and you do + not need the user redirected back after the logout completes. In that case the user is left + on a page controlled by the provider. + + Setting this to false is equivalent to leaving postLogoutRedirect unset. + type: boolean + uri: + description: |- + URI is sent as the `post_logout_redirect_uri` parameter to the OIDC Provider's end session + endpoint. The provider redirects the user to this URI after the logout completes, so it + usually must be pre-registered for the client with the provider. + + The URI may contain the Envoy "%REQ(header)%" + [command operator](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) + to build the URI from the request, for example + "%REQ(x-forwarded-proto)%://%REQ(:authority)%/loggedout". Envoy Gateway accepts only that + operator here, since it is the only one meaningful in a URI derived from the request, and + rejects any other so that a typo surfaces on this policy instead of being rejected by Envoy + as an invalid configuration. A literal percent is written as "%%". + + The scheme must be http, https, or a "%REQ(header)%" command operator. The URI is + percent-encoded automatically when the logout URL is built, so do not pre-encode it. + maxLength: 2048 + minLength: 1 + type: string + type: object + x-kubernetes-validations: + - message: exactly one of uri or disabled must be set + rule: has(self.uri) != has(self.disabled) provider: description: The OIDC Provider configuration. properties: diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 0e8d64a1a6..afaff7441c 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -952,6 +952,20 @@ func validateSecurityPolicy(p *egv1a1.SecurityPolicy) error { } } + // The CRD enforces that exactly one of uri or disabled is set, but CEL is evaluated per policy + // at admission and never on the result of a policy merge. Merging a parent policy that sets + // uri with a route policy that sets disabled (or the reverse) yields both, which the + // translation would silently resolve toward uri and so drop the more specific policy's intent. + // Checking it here also covers the paths that bypass CRD validation, e.g. the file provider. + if oidc != nil && oidc.PostLogoutRedirect != nil { + switch plr := oidc.PostLogoutRedirect; { + case plr.URI != nil && plr.Disabled != nil: + return errors.New("only one of OIDC.PostLogoutRedirect.uri or OIDC.PostLogoutRedirect.disabled must be set") + case plr.URI == nil && plr.Disabled == nil: + return errors.New("one of OIDC.PostLogoutRedirect.uri or OIDC.PostLogoutRedirect.disabled must be set") + } + } + basicAuth := p.Spec.BasicAuth if basicAuth != nil { if err := validateBasicAuth(basicAuth); err != nil { @@ -2064,6 +2078,11 @@ func (t *Translator) buildOIDC( if oidc.LogoutPath != nil { logoutPath = *oidc.LogoutPath } + if oidc.PostLogoutRedirect != nil && oidc.PostLogoutRedirect.URI != nil { + if err := validatePostLogoutRedirectURI(*oidc.PostLogoutRedirect.URI); err != nil { + return nil, err + } + } if oidc.ForwardAccessToken != nil { forwardAccessToken = *oidc.ForwardAccessToken } @@ -2112,6 +2131,7 @@ func (t *Translator) buildOIDC( RedirectURL: redirectURL, RedirectPath: redirectPath, LogoutPath: logoutPath, + PostLogoutRedirect: oidc.PostLogoutRedirect, ForwardAccessToken: forwardAccessToken, ForwardIDTokenHeader: forwardIDTokenHeader, RefreshToken: refreshToken, @@ -2288,6 +2308,103 @@ func extractRedirectPath(redirectURL string) (string, error) { return path, nil } +// validatePostLogoutRedirectURI sanity-checks the post logout redirect URI. Envoy evaluates the +// URI as a formatter string, so it may contain command operators and can't be parsed as a URL. +// This mirrors the scheme check in extractRedirectPath, but additionally accepts a request header +// command operator as the scheme, e.g. "%REQ(x-scheme)%://host/". +func validatePostLogoutRedirectURI(uri string) error { + schemeDelimiter := strings.Index(uri, "://") + if schemeDelimiter <= 0 { + return fmt.Errorf("invalid post logout redirect URI %s: must be an absolute URI", uri) + } + + // URI schemes are case-insensitive, see RFC 3986 section 3.1. + switch scheme := uri[:schemeDelimiter]; { + case strings.EqualFold(scheme, "http"), strings.EqualFold(scheme, "https"): + case isReqCommandOperator(scheme): + // The scheme is derived from a request header, e.g. x-forwarded-proto. + default: + return fmt.Errorf("invalid post logout redirect URI %s: scheme must be http, https, or a %%REQ(header)%% command operator", uri) + } + + // Unlike extractRedirectPath, a path is not required: the host root is a valid landing page. + // The authority is, since the provider would otherwise have nowhere to redirect to. + authority := uri[schemeDelimiter+len("://"):] + if end := strings.IndexAny(authority, "/?#"); end >= 0 { + authority = authority[:end] + } + if authority == "" { + return fmt.Errorf("invalid post logout redirect URI %s: must have a host", uri) + } + + if err := validateURICommandOperators(uri); err != nil { + return fmt.Errorf("invalid post logout redirect URI %s: %w", uri, err) + } + return nil +} + +// validateURICommandOperators checks every command operator in a URI that Envoy evaluates as a +// formatter string. Envoy fails to build the filter, and therefore rejects the whole +// configuration, when the string contains an unknown or malformed command operator. Catching that +// here surfaces the mistake on the SecurityPolicy status instead of NACKing the xDS update. +// +// Only %REQ(header)% is accepted: a URI is built from the inbound request, so the request header +// operator is the only one that is meaningful, and it is stable across Envoy versions. A literal +// percent is written as "%%", the same as in any Envoy format string. +func validateURICommandOperators(uri string) error { + for i := 0; i < len(uri); { + if uri[i] != '%' { + i++ + continue + } + // "%%" is an escaped literal percent, not the start of a command operator. + if i+1 < len(uri) && uri[i+1] == '%' { + i += 2 + continue + } + closing := strings.IndexByte(uri[i+1:], '%') + if closing < 0 { + return errors.New(`unterminated command operator, write a literal percent as "%%"`) + } + end := i + 1 + closing + 1 + if operator := uri[i:end]; !isReqCommandOperator(operator) { + return fmt.Errorf("unsupported command operator %s, only %%REQ(header)%% is supported", operator) + } + i = end + } + return nil +} + +// isReqCommandOperator reports whether s is an Envoy request header command operator, i.e. +// "%REQ(header)%" or "%REQ(header):maxLength%". +func isReqCommandOperator(s string) bool { + body, ok := strings.CutPrefix(s, "%REQ(") + if !ok { + return false + } + if body, ok = strings.CutSuffix(body, "%"); !ok { + return false + } + + header, maxLength, closed := strings.Cut(body, ")") + if !closed || header == "" || strings.ContainsAny(header, "%()") { + return false + } + if maxLength == "" { + return true + } + // An optional ":maxLength" truncates the header value, e.g. "%REQ(x-tenant):32%". + if maxLength, ok = strings.CutPrefix(maxLength, ":"); !ok || maxLength == "" { + return false + } + for _, c := range maxLength { + if c < '0' || c > '9' { + return false + } + } + return true +} + // appendOpenidScopeIfNotExist appends the openid scope to the provided scopes // if it is not already present. // `openid` is a required scope for OIDC. diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go index 5ff44c8d4c..cb5f5136f1 100644 --- a/internal/gatewayapi/securitypolicy_test.go +++ b/internal/gatewayapi/securitypolicy_test.go @@ -177,6 +177,170 @@ func Test_extractRedirectPath(t *testing.T) { } } +func Test_validatePostLogoutRedirectURI(t *testing.T) { + tests := []struct { + name string + uri string + wantErr bool + }{ + { + name: "https", + uri: "https://www.example.com/loggedout", + }, + { + name: "http", + uri: "http://www.example.com/", + }, + { + name: "with port", + uri: "https://www.example.com:9080/loggedout", + }, + { + name: "header value syntax", + uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/loggedout", + }, + { + // Unlike extractRedirectPath, the header is not restricted to x-forwarded-proto. + name: "any request header as scheme", + uri: "%REQ(x-scheme)%://www.example.com/loggedout", + }, + { + name: "pseudo header as scheme", + uri: "%REQ(:scheme)%://www.example.com/loggedout", + }, + { + // Envoy rejects the whole config on a malformed command operator, so these must + // not reach the xDS translation. + name: "empty command operator as scheme", + uri: "%%://www.example.com/loggedout", + wantErr: true, + }, + { + name: "unknown command operator as scheme", + uri: "%BOGUS%://www.example.com/loggedout", + wantErr: true, + }, + { + name: "request command operator without header", + uri: "%REQ()%://www.example.com/loggedout", + wantErr: true, + }, + { + name: "request command operator missing parentheses", + uri: "%REQ%://www.example.com/loggedout", + wantErr: true, + }, + { + name: "request command operator unterminated", + uri: "%REQ(x-scheme://www.example.com/loggedout", + wantErr: true, + }, + { + name: "command operators in host and path", + uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/%REQ(x-tenant)%/loggedout", + }, + { + name: "alternate header command operator", + uri: "https://%REQ(x-a?x-b)%/loggedout", + }, + { + name: "escaped literal percent", + uri: "https://www.example.com/logged%%20out", + }, + { + // Envoy only knows %REQ()% here; any other operator would fail filter creation + // and NACK the whole configuration. + name: "unsupported command operator in path", + uri: "https://www.example.com/%BOGUS%", + wantErr: true, + }, + { + name: "unsupported command operator in host", + uri: "https://%DOWNSTREAM_LOCAL_ADDRESS%/loggedout", + wantErr: true, + }, + { + name: "unterminated command operator in path", + uri: "https://www.example.com/%REQ(x-tenant)", + wantErr: true, + }, + { + name: "unpaired percent in path", + uri: "https://www.example.com/logged%20out", + wantErr: true, + }, + { + // The provider redirects to the host root, which is a valid post logout landing page. + name: "without path", + uri: "https://www.example.com/", + }, + { + name: "without trailing slash", + uri: "https://www.example.com", + }, + { + // URI schemes are case-insensitive, and Envoy passes the value through as-is. + name: "uppercase scheme", + uri: "HTTPS://www.example.com/loggedout", + }, + { + name: "truncated header command operator", + uri: "https://%REQ(x-tenant):32%/loggedout", + }, + { + name: "truncated header command operator with non-numeric length", + uri: "https://%REQ(x-tenant):abc%/loggedout", + wantErr: true, + }, + { + // The provider would have nowhere to redirect to. + name: "without host", + uri: "https://", + wantErr: true, + }, + { + name: "without host but with path", + uri: "https:///loggedout", + wantErr: true, + }, + { + name: "relative path", + uri: "/loggedout", + wantErr: true, + }, + { + name: "without scheme", + uri: "www.example.com/loggedout", + wantErr: true, + }, + { + name: "empty scheme", + uri: "://www.example.com/loggedout", + wantErr: true, + }, + { + name: "unsupported scheme", + uri: "htttps://www.example.com/loggedout", + wantErr: true, + }, + { + name: "empty", + uri: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePostLogoutRedirectURI(tt.uri) + if tt.wantErr { + require.Errorf(t, err, "validatePostLogoutRedirectURI(%v)", tt.uri) + return + } + require.NoErrorf(t, err, "validatePostLogoutRedirectURI(%v)", tt.uri) + }) + } +} + func Test_JWTProvider(t *testing.T) { tests := []struct { name string @@ -776,6 +940,51 @@ func Test_OIDC_PassThroughAuthHeader(t *testing.T) { } } +// Test_OIDC_PostLogoutRedirect_Merge covers the invariant that the CRD's CEL rule cannot: CEL is +// evaluated by the apiserver on an individual policy, but a merged policy only ever exists in +// memory inside the translator. Merging a parent that sets uri with a route that sets disabled +// therefore yields a policy that violates the exactly-one rule without any admission error, and +// the translator would resolve it toward uri, silently dropping the route's intent. +func Test_OIDC_PostLogoutRedirect_Merge(t *testing.T) { + oidcWith := func(plr *egv1a1.OIDCPostLogoutRedirect) *egv1a1.OIDC { + return &egv1a1.OIDC{PostLogoutRedirect: plr} + } + parentPolicy := func() *egv1a1.SecurityPolicy { + return &egv1a1.SecurityPolicy{ + Spec: egv1a1.SecurityPolicySpec{ + OIDC: oidcWith(&egv1a1.OIDCPostLogoutRedirect{ + URI: ToPointer("https://www.example.com/loggedout"), + }), + }, + } + } + routePolicy := func(mergeType *egv1a1.MergeType) *egv1a1.SecurityPolicy { + return &egv1a1.SecurityPolicy{ + Spec: egv1a1.SecurityPolicySpec{ + MergeType: mergeType, + OIDC: oidcWith(&egv1a1.OIDCPostLogoutRedirect{Disabled: ToPointer(true)}), + }, + } + } + + // Each policy on its own satisfies the exactly-one rule, which is why admission lets both + // through. + require.NoError(t, validateSecurityPolicy(parentPolicy())) + require.NoError(t, validateSecurityPolicy(routePolicy(nil))) + + strategicMerge := egv1a1.StrategicMerge + merged, _, err := mergeSecurityPolicy(routePolicy(&strategicMerge), parentPolicy()) + require.NoError(t, err) + + // Both fields survive the merge, so the merged policy must be rejected rather than silently + // resolved toward one of them. + plr := merged.Spec.OIDC.PostLogoutRedirect + require.NotNil(t, plr.URI, "expected the parent's uri to survive the merge") + require.NotNil(t, plr.Disabled, "expected the route's disabled to survive the merge") + require.ErrorContains(t, validateSecurityPolicy(merged), + "only one of OIDC.PostLogoutRedirect.uri or OIDC.PostLogoutRedirect.disabled must be set") +} + func ToPointer[T any](v T) *T { return &v } diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml index a60dc78e92..7060f26cda 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml @@ -142,6 +142,7 @@ securityPolicies: issuer: "https://oauth.foo.com" authorizationEndpoint: "https://oauth.foo.com/oauth2/v2/auth" tokenEndpoint: "https://oauth.foo.com/token" + endSessionEndpoint: "https://oauth.foo.com/logout" clientIDRef: name: "client2-secret" clientSecret: @@ -150,6 +151,8 @@ securityPolicies: resources: ["api"] redirectURL: "https://www.example.com/foo/oauth2/callback" logoutPath: "/foo/logout" + postLogoutRedirect: + uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/foo/loggedout" forwardAccessToken: true forwardIDToken: header: X-Id-Token # Forward the ID token on a custom header alongside the access token @@ -172,11 +175,14 @@ securityPolicies: provider: issuer: "https://accounts.google.com" authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth?foo=bar" # custom auth endpoint with query params, should be used as is + endSessionEndpoint: "https://accounts.google.com/o/oauth2/v2/logout" clientID: "client1.apps.googleusercontent.com" clientSecret: name: "client3-secret" redirectURL: "https://www.example.com/bar/oauth2/callback" logoutPath: "/bar/logout" + postLogoutRedirect: + disabled: true # omit the post_logout_redirect_uri parameter entirely forwardAccessToken: true defaultTokenTTL: 30m refreshToken: true diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml index 9396f9508a..3f85b9be7d 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml @@ -190,8 +190,11 @@ securityPolicies: forwardIDToken: header: X-Id-Token logoutPath: /foo/logout + postLogoutRedirect: + uri: '%REQ(x-forwarded-proto)%://%REQ(:authority)%/foo/loggedout' provider: authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth + endSessionEndpoint: https://oauth.foo.com/logout issuer: https://oauth.foo.com tokenEndpoint: https://oauth.foo.com/token redirectURL: https://www.example.com/foo/oauth2/callback @@ -241,8 +244,11 @@ securityPolicies: defaultTokenTTL: 30m forwardAccessToken: true logoutPath: /bar/logout + postLogoutRedirect: + disabled: true provider: authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth?foo=bar + endSessionEndpoint: https://accounts.google.com/o/oauth2/v2/logout issuer: https://accounts.google.com redirectURL: https://www.example.com/bar/oauth2/callback refreshToken: true @@ -405,8 +411,11 @@ xdsIR: hmacSecret: '[redacted]' logoutPath: /foo/logout name: securitypolicy/default/policy-for-http-route + postLogoutRedirect: + uri: '%REQ(x-forwarded-proto)%://%REQ(:authority)%/foo/loggedout' provider: authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth + endSessionEndpoint: https://oauth.foo.com/logout tokenEndpoint: https://oauth.foo.com/token redirectPath: /foo/oauth2/callback redirectURL: https://www.example.com/foo/oauth2/callback @@ -509,8 +518,11 @@ xdsIR: hmacSecret: '[redacted]' logoutPath: /bar/logout name: securitypolicy/default/policy-for-http-route-3 + postLogoutRedirect: + disabled: true provider: authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth?foo=bar + endSessionEndpoint: https://accounts.google.com/o/oauth2/v2/logout tokenEndpoint: https://oauth2.googleapis.com/token redirectPath: /bar/oauth2/callback redirectURL: https://www.example.com/bar/oauth2/callback diff --git a/internal/ir/xds.go b/internal/ir/xds.go index db6d2f0935..8dfd776414 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -1347,6 +1347,10 @@ type OIDC struct { // The path to log a user out, clearing their credential cookies. LogoutPath string `json:"logoutPath,omitempty"` + // PostLogoutRedirect configures the `post_logout_redirect_uri` parameter sent to the OIDC + // Provider's end session endpoint. If nil, Envoy's default is used. + PostLogoutRedirect *egv1a1.OIDCPostLogoutRedirect `json:"postLogoutRedirect,omitempty"` + // ForwardAccessToken indicates whether the Envoy should forward the access token // via the Authorization header Bearer scheme to the upstream. ForwardAccessToken bool `json:"forwardAccessToken,omitempty"` diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index cfbfc5a62d..cb6af645e3 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -3292,6 +3292,11 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.PostLogoutRedirect != nil { + in, out := &in.PostLogoutRedirect, &out.PostLogoutRedirect + *out = new(v1alpha1.OIDCPostLogoutRedirect) + (*in).DeepCopyInto(*out) + } if in.ForwardIDTokenHeader != nil { in, out := &in.ForwardIDTokenHeader, &out.ForwardIDTokenHeader *out = new(string) diff --git a/internal/xds/translator/oidc.go b/internal/xds/translator/oidc.go index fba5f7da53..2d9150b6a5 100644 --- a/internal/xds/translator/oidc.go +++ b/internal/xds/translator/oidc.go @@ -19,6 +19,7 @@ import ( "github.com/golang/protobuf/ptypes/wrappers" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/durationpb" + "k8s.io/utils/ptr" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/envoyproxy/gateway/internal/ir" @@ -225,6 +226,23 @@ func oauth2Config(securityFeatures *ir.SecurityFeatures) (*oauth2v3.OAuth2PerRou oauth2.Config.EndSessionEndpoint = *oidc.Provider.EndSessionEndpoint } + // The post_logout_redirect_uri is only meaningful when an end session endpoint is configured; + // Envoy ignores it otherwise. If left unset, Envoy falls back to ":///". + // Envoy's oneof requires exactly one branch to be set, and disabled is constrained to true, so + // neither an empty message nor disabled=false may be emitted. + if plr := oidc.PostLogoutRedirect; plr != nil { + switch { + case plr.URI != nil: + oauth2.Config.PostLogoutRedirectUri = &oauth2v3.PostLogoutRedirectUri{ + Config: &oauth2v3.PostLogoutRedirectUri_Uri{Uri: *plr.URI}, + } + case ptr.Deref(plr.Disabled, false): + oauth2.Config.PostLogoutRedirectUri = &oauth2v3.PostLogoutRedirectUri{ + Config: &oauth2v3.PostLogoutRedirectUri_Disabled{Disabled: true}, + } + } + } + if oidc.CSRFTokenTTL != nil { oauth2.Config.CsrfTokenExpiresIn = durationpb.New(oidc.CSRFTokenTTL.Duration) } diff --git a/internal/xds/translator/testdata/in/xds-ir/oidc.yaml b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml index 24adbb27c3..6ef00fefff 100644 --- a/internal/xds/translator/testdata/in/xds-ir/oidc.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml @@ -28,6 +28,7 @@ http: provider: authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth tokenEndpoint: https://oauth.foo.com/token + endSessionEndpoint: https://oauth.foo.com/logout scopes: - openid - email @@ -37,6 +38,9 @@ http: redirectURL: "https://www.example.com/foo/oauth2/callback" redirectPath: "/foo/oauth2/callback" logoutPath: "/foo/logout" + # Custom post_logout_redirect_uri, with Envoy command operators. + postLogoutRedirect: + uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/foo/loggedout" cookieSuffix: 5F93C2E4 forwardAccessToken: true # Forward the ID token on a custom header. This coexists with @@ -66,6 +70,7 @@ http: provider: authorizationEndpoint: https://oauth.bar.com/oauth2/v2/auth tokenEndpoint: https://oauth.bar.com/token + endSessionEndpoint: https://oauth.bar.com/logout scopes: - openid - email @@ -91,6 +96,9 @@ http: - name: test-no-type value: foobar logoutPath: "/bar/logout" + # Omit the post_logout_redirect_uri parameter entirely. + postLogoutRedirect: + disabled: true cookieSuffix: 5f93c2e4 cookieNameOverrides: idToken: "CustomIdTokenOverride" diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc.routes.yaml index a9a8524d1f..8bcda3017d 100644 --- a/internal/xds/translator/testdata/out/xds-ir/oidc.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/oidc.routes.yaml @@ -44,9 +44,12 @@ csrfTokenExpiresIn: 2100s defaultExpiresIn: 3600s defaultRefreshTokenExpiresIn: 172800s + endSessionEndpoint: https://oauth.foo.com/logout forwardBearerToken: true forwardIdToken: header: X-ID-Token + postLogoutRedirectUri: + uri: '%REQ(x-forwarded-proto)%://%REQ(:authority)%/foo/loggedout' redirectPathMatcher: path: exact: /foo/oauth2/callback @@ -117,8 +120,11 @@ stringMatch: exact: foobar disableTokenEncryption: true + endSessionEndpoint: https://oauth.bar.com/logout forwardIdToken: header: Authorization + postLogoutRedirectUri: + disabled: true redirectPathMatcher: path: exact: /bar/oauth2/callback diff --git a/release-notes/current/new_features/7349-oidc-post-logout-redirect-uri.md b/release-notes/current/new_features/7349-oidc-post-logout-redirect-uri.md new file mode 100644 index 0000000000..ea1a02293e --- /dev/null +++ b/release-notes/current/new_features/7349-oidc-post-logout-redirect-uri.md @@ -0,0 +1 @@ +Added `spec.oidc.postLogoutRedirect` to SecurityPolicy to configure the `post_logout_redirect_uri` parameter EG sends to the OIDC Provider during RP-Initiated Logout, either by setting an explicit `uri` or by setting `disabled` to omit the parameter entirely. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index ad331bbbdc..85007f0fbc 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -4264,6 +4264,7 @@ _Appears in:_ | `redirectURL` | _string_ | true | | The redirect URL to be used in the OIDC
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, uses the default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" | | `denyRedirect` | _[OIDCDenyRedirect](#oidcdenyredirect)_ | false | | Any request that matches any of the provided matchers (with either tokens that are expired or missing tokens) will not be redirected to the OIDC Provider.
This behavior can be useful for AJAX or machine requests. | | `logoutPath` | _string_ | true | | The path to log a user out, clearing their credential cookies.
If not specified, uses a default logout path "/logout" | +| `postLogoutRedirect` | _[OIDCPostLogoutRedirect](#oidcpostlogoutredirect)_ | false | | PostLogoutRedirect configures the `post_logout_redirect_uri` parameter that EG sends to the
OIDC Provider's end session endpoint when a user accesses the logout path.
This only applies when the OIDC Provider's end session endpoint is configured or discovered,
i.e. when RP-Initiated Logout is in use. It is ignored otherwise.
If not specified, Envoy sends the root of the request's host, ":///".
Note that many OIDC Providers require the post logout redirect URI to be pre-registered for
the client and reject the logout request otherwise. If the default value is not registered,
set an explicit uri here, or set disabled to true to omit the parameter altogether. | | `forwardAccessToken` | _boolean_ | false | | ForwardAccessToken indicates whether the Envoy should forward the access token
via the Authorization header Bearer scheme to the upstream.
If not specified, defaults to false. | | `forwardIDToken` | _[OIDCTokenForwarding](#oidctokenforwarding)_ | false | | ForwardIDToken configures forwarding of the OIDC ID token to the upstream.
If the configured header is "Authorization", EG forwards the ID token using
the "Bearer " prefix. For any other header, EG forwards the raw token value.
If not specified, the ID token will not be forwarded.
Note: when passThroughAuthHeader is enabled, this header must not be the same
as a header a JWT provider extracts from (the "Authorization" header by
default). The forwarded ID token header is owned by Envoy, and Envoy rejects
an OAuth2 configuration whose pass-through matcher keys on it. | | `defaultTokenTTL` | _[Duration](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#duration)_ | false | | DefaultTokenTTL is the default lifetime of the id token and access token.
Please note that Envoy will always use the expiry time from the response
of the authorization server if it is provided. This field is only used when
the expiry time is not provided by the authorization.
If not specified, defaults to 0. In this case, the "expires_in" field in
the authorization response must be set by the authorization server, or the
OAuth flow will fail. | @@ -4333,6 +4334,24 @@ _Appears in:_ | `value` | _string_ | true | | Value specifies the string value that the match must have. | +#### OIDCPostLogoutRedirect + + + +OIDCPostLogoutRedirect configures the `post_logout_redirect_uri` parameter used in OIDC +[RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) requests. + +Exactly one of uri or disabled must be set. + +_Appears in:_ +- [OIDC](#oidc) + +| Field | Type | Required | Default | Description | +| --- | --- | --- | --- | --- | +| `uri` | _string_ | false | | URI is sent as the `post_logout_redirect_uri` parameter to the OIDC Provider's end session
endpoint. The provider redirects the user to this URI after the logout completes, so it
usually must be pre-registered for the client with the provider.
The URI may contain the Envoy "%REQ(header)%"
[command operator](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators)
to build the URI from the request, for example
"%REQ(x-forwarded-proto)%://%REQ(:authority)%/loggedout". Envoy Gateway accepts only that
operator here, since it is the only one meaningful in a URI derived from the request, and
rejects any other so that a typo surfaces on this policy instead of being rejected by Envoy
as an invalid configuration. A literal percent is written as "%%".
The scheme must be http, https, or a "%REQ(header)%" command operator. The URI is
percent-encoded automatically when the logout URL is built, so do not pre-encode it. | +| `disabled` | _boolean_ | false | | Disabled omits the `post_logout_redirect_uri` parameter from the logout request entirely.
Use this when the OIDC Provider rejects unregistered post logout redirect URIs and you do
not need the user redirected back after the logout completes. In that case the user is left
on a page controlled by the provider.
Setting this to false is equivalent to leaving postLogoutRedirect unset. | + + #### OIDCProvider diff --git a/site/content/en/latest/tasks/security/oidc.md b/site/content/en/latest/tasks/security/oidc.md index 9038e0604c..e8b79f389d 100644 --- a/site/content/en/latest/tasks/security/oidc.md +++ b/site/content/en/latest/tasks/security/oidc.md @@ -542,6 +542,83 @@ Additional connection settings for the OIDC provider can be configured in the [b For more information about [Backend] and [BackendTLSPolicy], refer to the [Backend Routing][backend-routing] and [Backend TLS: Gateway to Backend][backend-tls] tasks. +## Logout + +Requesting the `logoutPath` clears the OIDC cookies Envoy Gateway set for the session. If not +specified, `logoutPath` defaults to `/logout`. Like `redirectURL`, it must match the target +HTTPRoute or Gateway, otherwise the logout request can't be processed by the OIDC filter. + +Clearing the cookies only ends the session at the gateway; the user remains logged in at the OIDC +provider. To also terminate the provider-side session, configure the provider's +[end session endpoint](https://openid.net/specs/openid-connect-rpinitiated-1_0.html), which enables +[RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html): + +```yaml + oidc: + provider: + issuer: "https://accounts.google.com" + endSessionEndpoint: "https://accounts.google.com/o/oauth2/v2/logout" + logoutPath: "/myapp/logout" +``` + +Envoy Gateway also discovers `endSessionEndpoint` from the provider's +[Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse) +when `authorizationEndpoint` or `tokenEndpoint` is not specified, so RP-Initiated Logout may be +active even if you did not set the field explicitly. + +When an end session endpoint is in use, requesting the `logoutPath` returns a redirect to the +provider that carries a `post_logout_redirect_uri` parameter telling the provider where to send the +user once the logout completes. By default Envoy sends the root of the request's host, +`:///`. + +### Setting the post logout redirect URI + +Most providers require the post logout redirect URI to be **registered for the client** and reject +the logout request otherwise. If the default value is not registered, set an explicit URI: + +```yaml + oidc: + logoutPath: "/myapp/logout" + postLogoutRedirect: + uri: "https://www.example.com/myapp/loggedout" +``` + +The URI may contain the Envoy `%REQ(header)%` [command operator][command-operators], which is useful +when the same policy serves more than one host: + +```yaml + oidc: + postLogoutRedirect: + uri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/myapp/loggedout" +``` + +Only `%REQ(header)%` is accepted here, since it is the only operator meaningful in a URI derived from +the request. Envoy Gateway rejects any other operator and reports the error on the SecurityPolicy +status, so that a typo such as `%REQ(x-tenant)` or `%BOGUS%` surfaces on the policy rather than +causing Envoy to reject the configuration. Envoy percent-encodes the URI when it builds the logout +redirect, so write it unencoded; a literal percent is written as `%%`. + +### Omitting the post logout redirect URI + +`post_logout_redirect_uri` is optional in the OIDC specification. If you cannot register a URI with +your provider, or you do not need the user redirected back to your application, omit the parameter +entirely: + +```yaml + oidc: + logoutPath: "/myapp/logout" + postLogoutRedirect: + disabled: true +``` + +The logout itself still completes — Envoy Gateway clears its cookies and the provider terminates the +session — but because the provider is not told where to send the user, the browser is left on a page +controlled by the provider rather than returning to your application. + +Exactly one of `uri` or `disabled` must be set. `postLogoutRedirect` has no effect when no end +session endpoint is configured or discovered, because in that case Envoy Gateway does not redirect to +the provider at all. + ## Providers Guides to integrate with specific OIDC providers. @@ -627,3 +704,4 @@ Checkout the [Developer Guide](/community/develop) to get involved in the projec [backend-routing]: ../traffic/backend [backend-tls]: ../backend-tls [BackendSettings]: ../../../api/extension_types/#clustersettings +[command-operators]: https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators diff --git a/test/cel-validation/securitypolicy_test.go b/test/cel-validation/securitypolicy_test.go index c834d5f535..451e87bf13 100644 --- a/test/cel-validation/securitypolicy_test.go +++ b/test/cel-validation/securitypolicy_test.go @@ -2046,6 +2046,213 @@ func TestSecurityPolicyTarget(t *testing.T) { }, wantErrors: []string{"forwardAccessToken cannot be true when forwardIDToken.header is Authorization"}, }, + { + desc: "oidc-post-logout-redirect-uri", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Group: new(gwapiv1.Group("gateway.networking.k8s.io")), + Kind: "HTTPRoute", + MatchLabels: map[string]string{"eg/namespace": "reference-apps"}, + }, + }, + }, + OIDC: &egv1a1.OIDC{ + Provider: egv1a1.OIDCProvider{ + Issuer: "https://accounts.google.com", + AuthorizationEndpoint: new("https://accounts.google.com/o/oauth2/v2/auth"), + TokenEndpoint: new("https://oauth2.googleapis.com/token"), + EndSessionEndpoint: new("https://accounts.google.com/o/oauth2/v2/logout"), + }, + ClientID: new("client-id"), + ClientSecret: gwapiv1b1.SecretObjectReference{Name: "secret"}, + PostLogoutRedirect: &egv1a1.OIDCPostLogoutRedirect{ + URI: new("https://www.example.com/loggedout"), + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "oidc-post-logout-redirect-uri-command-operator", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Group: new(gwapiv1.Group("gateway.networking.k8s.io")), + Kind: "HTTPRoute", + MatchLabels: map[string]string{"eg/namespace": "reference-apps"}, + }, + }, + }, + OIDC: &egv1a1.OIDC{ + Provider: egv1a1.OIDCProvider{ + Issuer: "https://accounts.google.com", + AuthorizationEndpoint: new("https://accounts.google.com/o/oauth2/v2/auth"), + TokenEndpoint: new("https://oauth2.googleapis.com/token"), + }, + ClientID: new("client-id"), + ClientSecret: gwapiv1b1.SecretObjectReference{Name: "secret"}, + // Envoy command operators are a valid URI value, so the CRD must not + // constrain this field to a well-formed URI. + PostLogoutRedirect: &egv1a1.OIDCPostLogoutRedirect{ + URI: new("%REQ(x-forwarded-proto)%://%REQ(:authority)%/loggedout"), + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "oidc-post-logout-redirect-disabled", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Group: new(gwapiv1.Group("gateway.networking.k8s.io")), + Kind: "HTTPRoute", + MatchLabels: map[string]string{"eg/namespace": "reference-apps"}, + }, + }, + }, + OIDC: &egv1a1.OIDC{ + Provider: egv1a1.OIDCProvider{ + Issuer: "https://accounts.google.com", + AuthorizationEndpoint: new("https://accounts.google.com/o/oauth2/v2/auth"), + TokenEndpoint: new("https://oauth2.googleapis.com/token"), + }, + ClientID: new("client-id"), + ClientSecret: gwapiv1b1.SecretObjectReference{Name: "secret"}, + PostLogoutRedirect: &egv1a1.OIDCPostLogoutRedirect{ + Disabled: new(true), + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "oidc-post-logout-redirect-disabled-false", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Group: new(gwapiv1.Group("gateway.networking.k8s.io")), + Kind: "HTTPRoute", + MatchLabels: map[string]string{"eg/namespace": "reference-apps"}, + }, + }, + }, + OIDC: &egv1a1.OIDC{ + Provider: egv1a1.OIDCProvider{ + Issuer: "https://accounts.google.com", + AuthorizationEndpoint: new("https://accounts.google.com/o/oauth2/v2/auth"), + TokenEndpoint: new("https://oauth2.googleapis.com/token"), + }, + ClientID: new("client-id"), + ClientSecret: gwapiv1b1.SecretObjectReference{Name: "secret"}, + // Explicitly false is accepted and means the same as leaving + // postLogoutRedirect unset. + PostLogoutRedirect: &egv1a1.OIDCPostLogoutRedirect{ + Disabled: new(false), + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "oidc-post-logout-redirect-uri-and-disabled", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Group: new(gwapiv1.Group("gateway.networking.k8s.io")), + Kind: "HTTPRoute", + MatchLabels: map[string]string{"eg/namespace": "reference-apps"}, + }, + }, + }, + OIDC: &egv1a1.OIDC{ + Provider: egv1a1.OIDCProvider{ + Issuer: "https://accounts.google.com", + AuthorizationEndpoint: new("https://accounts.google.com/o/oauth2/v2/auth"), + TokenEndpoint: new("https://oauth2.googleapis.com/token"), + }, + ClientID: new("client-id"), + ClientSecret: gwapiv1b1.SecretObjectReference{Name: "secret"}, + PostLogoutRedirect: &egv1a1.OIDCPostLogoutRedirect{ + URI: new("https://www.example.com/loggedout"), + Disabled: new(true), + }, + }, + } + }, + wantErrors: []string{"exactly one of uri or disabled must be set"}, + }, + { + desc: "oidc-post-logout-redirect-empty", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Group: new(gwapiv1.Group("gateway.networking.k8s.io")), + Kind: "HTTPRoute", + MatchLabels: map[string]string{"eg/namespace": "reference-apps"}, + }, + }, + }, + OIDC: &egv1a1.OIDC{ + Provider: egv1a1.OIDCProvider{ + Issuer: "https://accounts.google.com", + AuthorizationEndpoint: new("https://accounts.google.com/o/oauth2/v2/auth"), + TokenEndpoint: new("https://oauth2.googleapis.com/token"), + }, + ClientID: new("client-id"), + ClientSecret: gwapiv1b1.SecretObjectReference{Name: "secret"}, + PostLogoutRedirect: &egv1a1.OIDCPostLogoutRedirect{}, + }, + } + }, + wantErrors: []string{"exactly one of uri or disabled must be set"}, + }, + { + desc: "oidc-post-logout-redirect-empty-uri", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + PolicyTargetReferences: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Group: new(gwapiv1.Group("gateway.networking.k8s.io")), + Kind: "HTTPRoute", + MatchLabels: map[string]string{"eg/namespace": "reference-apps"}, + }, + }, + }, + OIDC: &egv1a1.OIDC{ + Provider: egv1a1.OIDCProvider{ + Issuer: "https://accounts.google.com", + AuthorizationEndpoint: new("https://accounts.google.com/o/oauth2/v2/auth"), + TokenEndpoint: new("https://oauth2.googleapis.com/token"), + }, + ClientID: new("client-id"), + ClientSecret: gwapiv1b1.SecretObjectReference{Name: "secret"}, + PostLogoutRedirect: &egv1a1.OIDCPostLogoutRedirect{ + URI: new(""), + }, + }, + } + }, + wantErrors: []string{"should be at least 1 chars long"}, + }, } for _, tc := range cases { diff --git a/test/e2e/testdata/oidc-securitypolicy.yaml b/test/e2e/testdata/oidc-securitypolicy.yaml index 22c86bd874..85a0aea123 100644 --- a/test/e2e/testdata/oidc-securitypolicy.yaml +++ b/test/e2e/testdata/oidc-securitypolicy.yaml @@ -96,6 +96,8 @@ spec: name: "oidctest-foo-secret" redirectURL: "http://www.example.com/foo/oauth2/callback" logoutPath: "/foo/logout" + postLogoutRedirect: + uri: "http://www.example.com/foo/loggedout" # sent as post_logout_redirect_uri on the logout redirect forwardAccessToken: true passThroughAuthHeader: true jwt: @@ -138,6 +140,8 @@ spec: name: "oidctest-bar-secret" redirectURL: "http://www.example.com/bar/oauth2/callback" logoutPath: "/bar/logout" + postLogoutRedirect: + disabled: true # omit post_logout_redirect_uri from the logout redirect forwardAccessToken: true passThroughAuthHeader: true forwardIDToken: diff --git a/test/e2e/tests/oidc.go b/test/e2e/tests/oidc.go index 6668e55648..cc09b98644 100644 --- a/test/e2e/tests/oidc.go +++ b/test/e2e/tests/oidc.go @@ -48,6 +48,12 @@ type oidcRouteTestCase struct { // forwardedIDTokenHeader, when set, is the request header that EG is configured // to forward the OIDC ID token on. The test verifies the upstream receives it. forwardedIDTokenHeader string + // expectedPostLogoutRedirectURI, when set, is the percent-encoded value expected + // in the post_logout_redirect_uri parameter of the logout redirect. + expectedPostLogoutRedirectURI string + // expectNoPostLogoutRedirectURI asserts the post_logout_redirect_uri parameter is + // absent from the logout redirect, i.e. postLogoutRedirect.disabled is set. + expectNoPostLogoutRedirectURI bool } func init() { @@ -75,13 +81,16 @@ var OIDCTest = suite.ConformanceTest{ securityPolicyName: "oidc-test-foo", testURL: "http://www.example.com/foo", logoutURL: "http://www.example.com/foo/logout", + // Envoy percent-encodes everything but ALPHA, DIGIT and "*-._". + expectedPostLogoutRedirectURI: "http%3A%2F%2Fwww.example.com%2Ffoo%2Floggedout", }, { - routeName: "http-with-oidc-bar", - securityPolicyName: "oidc-test-bar", - testURL: "http://www.example.com/bar", - logoutURL: "http://www.example.com/bar/logout", - forwardedIDTokenHeader: "X-Id-Token", + routeName: "http-with-oidc-bar", + securityPolicyName: "oidc-test-bar", + testURL: "http://www.example.com/bar", + logoutURL: "http://www.example.com/bar/logout", + forwardedIDTokenHeader: "X-Id-Token", + expectNoPostLogoutRedirectURI: true, }, } @@ -169,6 +178,9 @@ var OIDCTest = suite.ConformanceTest{ securityPolicyName: "oidc-test", testURL: "http://www.example.com/myapp", logoutURL: "http://www.example.com/myapp/logout", + // This policy sets no postLogoutRedirect, so Envoy falls back to the root of + // the request's host. + expectedPostLogoutRedirectURI: "http%3A%2F%2Fwww.example.com%2F", }, "testdata/oidc-securitypolicy-backendcluster.yaml") }) }, @@ -278,7 +290,22 @@ func testOIDC(t *testing.T, suite *suite.ConformanceTestSuite, tc *oidcRouteTest require.Equal(t, http.StatusFound, res.StatusCode) // After logout, OAuth2 filter will redirect to the IdP end session endpoint. - require.Contains(t, res.Header.Get("Location"), "https://keycloak.gateway-conformance-infra/realms/master/protocol/openid-connect/logout", "Expected redirect to the root of the host") + location := res.Header.Get("Location") + require.Contains(t, location, "https://keycloak.gateway-conformance-infra/realms/master/protocol/openid-connect/logout", "Expected redirect to the root of the host") + + // The post_logout_redirect_uri parameter on that redirect is controlled by + // oidc.postLogoutRedirect in the SecurityPolicy. Every test case must declare which behavior + // it expects, so that a new case can't silently assert nothing here. + switch { + case tc.expectNoPostLogoutRedirectURI: + require.NotContains(t, location, "post_logout_redirect_uri", + "Expected the post_logout_redirect_uri parameter to be omitted") + case tc.expectedPostLogoutRedirectURI != "": + require.Contains(t, location, "post_logout_redirect_uri="+tc.expectedPostLogoutRedirectURI, + "Expected the configured post_logout_redirect_uri on the logout redirect") + default: + t.Fatal("test case must set expectedPostLogoutRedirectURI or expectNoPostLogoutRedirectURI") + } // Verify that the oauth2 cookies have been deleted var cookieDeleted bool diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index 668e846361..b876788616 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -58362,6 +58362,51 @@ spec: If not specified, defaults to false. type: boolean + postLogoutRedirect: + description: |- + PostLogoutRedirect configures the `post_logout_redirect_uri` parameter that EG sends to the + OIDC Provider's end session endpoint when a user accesses the logout path. + + This only applies when the OIDC Provider's end session endpoint is configured or discovered, + i.e. when RP-Initiated Logout is in use. It is ignored otherwise. + + If not specified, Envoy sends the root of the request's host, ":///". + Note that many OIDC Providers require the post logout redirect URI to be pre-registered for + the client and reject the logout request otherwise. If the default value is not registered, + set an explicit uri here, or set disabled to true to omit the parameter altogether. + properties: + disabled: + description: |- + Disabled omits the `post_logout_redirect_uri` parameter from the logout request entirely. + Use this when the OIDC Provider rejects unregistered post logout redirect URIs and you do + not need the user redirected back after the logout completes. In that case the user is left + on a page controlled by the provider. + + Setting this to false is equivalent to leaving postLogoutRedirect unset. + type: boolean + uri: + description: |- + URI is sent as the `post_logout_redirect_uri` parameter to the OIDC Provider's end session + endpoint. The provider redirects the user to this URI after the logout completes, so it + usually must be pre-registered for the client with the provider. + + The URI may contain the Envoy "%REQ(header)%" + [command operator](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) + to build the URI from the request, for example + "%REQ(x-forwarded-proto)%://%REQ(:authority)%/loggedout". Envoy Gateway accepts only that + operator here, since it is the only one meaningful in a URI derived from the request, and + rejects any other so that a typo surfaces on this policy instead of being rejected by Envoy + as an invalid configuration. A literal percent is written as "%%". + + The scheme must be http, https, or a "%REQ(header)%" command operator. The URI is + percent-encoded automatically when the logout URL is built, so do not pre-encode it. + maxLength: 2048 + minLength: 1 + type: string + type: object + x-kubernetes-validations: + - message: exactly one of uri or disabled must be set + rule: has(self.uri) != has(self.disabled) provider: description: The OIDC Provider configuration. properties: diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 7dcc378deb..bd60bd9db8 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -34300,6 +34300,51 @@ spec: If not specified, defaults to false. type: boolean + postLogoutRedirect: + description: |- + PostLogoutRedirect configures the `post_logout_redirect_uri` parameter that EG sends to the + OIDC Provider's end session endpoint when a user accesses the logout path. + + This only applies when the OIDC Provider's end session endpoint is configured or discovered, + i.e. when RP-Initiated Logout is in use. It is ignored otherwise. + + If not specified, Envoy sends the root of the request's host, ":///". + Note that many OIDC Providers require the post logout redirect URI to be pre-registered for + the client and reject the logout request otherwise. If the default value is not registered, + set an explicit uri here, or set disabled to true to omit the parameter altogether. + properties: + disabled: + description: |- + Disabled omits the `post_logout_redirect_uri` parameter from the logout request entirely. + Use this when the OIDC Provider rejects unregistered post logout redirect URIs and you do + not need the user redirected back after the logout completes. In that case the user is left + on a page controlled by the provider. + + Setting this to false is equivalent to leaving postLogoutRedirect unset. + type: boolean + uri: + description: |- + URI is sent as the `post_logout_redirect_uri` parameter to the OIDC Provider's end session + endpoint. The provider redirects the user to this URI after the logout completes, so it + usually must be pre-registered for the client with the provider. + + The URI may contain the Envoy "%REQ(header)%" + [command operator](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) + to build the URI from the request, for example + "%REQ(x-forwarded-proto)%://%REQ(:authority)%/loggedout". Envoy Gateway accepts only that + operator here, since it is the only one meaningful in a URI derived from the request, and + rejects any other so that a typo surfaces on this policy instead of being rejected by Envoy + as an invalid configuration. A literal percent is written as "%%". + + The scheme must be http, https, or a "%REQ(header)%" command operator. The URI is + percent-encoded automatically when the logout URL is built, so do not pre-encode it. + maxLength: 2048 + minLength: 1 + type: string + type: object + x-kubernetes-validations: + - message: exactly one of uri or disabled must be set + rule: has(self.uri) != has(self.disabled) provider: description: The OIDC Provider configuration. properties: diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml index e0adf5953f..e65790da00 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -34300,6 +34300,51 @@ spec: If not specified, defaults to false. type: boolean + postLogoutRedirect: + description: |- + PostLogoutRedirect configures the `post_logout_redirect_uri` parameter that EG sends to the + OIDC Provider's end session endpoint when a user accesses the logout path. + + This only applies when the OIDC Provider's end session endpoint is configured or discovered, + i.e. when RP-Initiated Logout is in use. It is ignored otherwise. + + If not specified, Envoy sends the root of the request's host, ":///". + Note that many OIDC Providers require the post logout redirect URI to be pre-registered for + the client and reject the logout request otherwise. If the default value is not registered, + set an explicit uri here, or set disabled to true to omit the parameter altogether. + properties: + disabled: + description: |- + Disabled omits the `post_logout_redirect_uri` parameter from the logout request entirely. + Use this when the OIDC Provider rejects unregistered post logout redirect URIs and you do + not need the user redirected back after the logout completes. In that case the user is left + on a page controlled by the provider. + + Setting this to false is equivalent to leaving postLogoutRedirect unset. + type: boolean + uri: + description: |- + URI is sent as the `post_logout_redirect_uri` parameter to the OIDC Provider's end session + endpoint. The provider redirects the user to this URI after the logout completes, so it + usually must be pre-registered for the client with the provider. + + The URI may contain the Envoy "%REQ(header)%" + [command operator](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#command-operators) + to build the URI from the request, for example + "%REQ(x-forwarded-proto)%://%REQ(:authority)%/loggedout". Envoy Gateway accepts only that + operator here, since it is the only one meaningful in a URI derived from the request, and + rejects any other so that a typo surfaces on this policy instead of being rejected by Envoy + as an invalid configuration. A literal percent is written as "%%". + + The scheme must be http, https, or a "%REQ(header)%" command operator. The URI is + percent-encoded automatically when the logout URL is built, so do not pre-encode it. + maxLength: 2048 + minLength: 1 + type: string + type: object + x-kubernetes-validations: + - message: exactly one of uri or disabled must be set + rule: has(self.uri) != has(self.disabled) provider: description: The OIDC Provider configuration. properties: