From 498f2b91be59722aa9aeeec5b61f2f8ac58401ac Mon Sep 17 00:00:00 2001 From: "Huabing (Robin) Zhao" Date: Mon, 3 Aug 2026 00:23:52 -0700 Subject: [PATCH] feat: add codeVerifierTTL to the OIDC SecurityPolicy csrfTokenTTL already bounds the lifetime of the nonce cookie Envoy sets when it starts an authorization flow, but there was no equivalent for the PKCE code verifier cookie, which stayed pinned at the oauth2 filter's 600s default. Both cookies are only consumed on the callback endpoint, so a flow that is started but never completed leaves them in the browser until they expire. Exposing code_verifier_token_expires_in lets an operator bound that window for the code verifier the same way csrfTokenTTL already does for the nonce. Signed-off-by: Huabing (Robin) Zhao --- api/v1alpha1/oidc_types.go | 18 ++++++++++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 5 +++++ ...gateway.envoyproxy.io_securitypolicies.yaml | 18 ++++++++++++++++++ ...gateway.envoyproxy.io_securitypolicies.yaml | 18 ++++++++++++++++++ internal/gatewayapi/securitypolicy.go | 8 ++++++++ .../testdata/securitypolicy-with-oidc.in.yaml | 1 + .../testdata/securitypolicy-with-oidc.out.yaml | 2 ++ internal/ir/xds.go | 3 +++ internal/ir/zz_generated.deepcopy.go | 5 +++++ internal/xds/translator/oidc.go | 4 ++++ .../translator/testdata/in/xds-ir/oidc.yaml | 1 + .../testdata/out/xds-ir/oidc.routes.yaml | 1 + .../9645-oidc-code-verifier-ttl.md | 3 +++ site/content/en/latest/api/extension_types.md | 1 + test/helm/gateway-crds-helm/all.out.yaml | 18 ++++++++++++++++++ test/helm/gateway-crds-helm/e2e.out.yaml | 18 ++++++++++++++++++ .../envoy-gateway-crds.out.yaml | 18 ++++++++++++++++++ 17 files changed, 142 insertions(+) create mode 100644 release-notes/current/new_features/9645-oidc-code-verifier-ttl.md diff --git a/api/v1alpha1/oidc_types.go b/api/v1alpha1/oidc_types.go index 910813feae..09275b063b 100644 --- a/api/v1alpha1/oidc_types.go +++ b/api/v1alpha1/oidc_types.go @@ -156,6 +156,24 @@ type OIDC struct { // +optional CSRFTokenTTL *gwapiv1.Duration `json:"csrfTokenTTL,omitempty"` + // CodeVerifierTTL defines how long the PKCE code verifier generated during the OAuth2 + // authorization flow remains valid. + // + // This duration determines the lifetime of the code verifier cookie, which is exchanged + // for the access token when the provider redirects back to the callback endpoint. + // + // Both this cookie and the CSRF cookie are only consumed on the callback endpoint, so an + // authorization flow that is started but never completed leaves them behind until they + // expire. Lowering this value along with csrfTokenTTL bounds how many such cookies a + // browser can accumulate. It must stay long enough for a user to complete the login, + // including any multi-factor prompt: once it elapses the browser drops the cookie and + // the callback fails. + // + // If omitted, Envoy Gateway defaults the code verifier expiration to 10 minutes. + // + // +optional + CodeVerifierTTL *gwapiv1.Duration `json:"codeVerifierTTL,omitempty"` + // Disable token encryption. When set to true, both the access token and the ID token will be stored in plain text. // This option should only be used in secure environments where token encryption is not required. // Default is false (tokens are encrypted). diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 7730ebed2b..c05d0d6ab7 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -6254,6 +6254,11 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { *out = new(v1.Duration) **out = **in } + if in.CodeVerifierTTL != nil { + in, out := &in.CodeVerifierTTL, &out.CodeVerifierTTL + *out = new(v1.Duration) + **out = **in + } if in.DisableTokenEncryption != nil { in, out := &in.DisableTokenEncryption, &out.DisableTokenEncryption *out = new(bool) 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 5ba59962e9..168218eda5 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 @@ -5782,6 +5782,24 @@ spec: required: - name type: object + codeVerifierTTL: + description: |- + CodeVerifierTTL defines how long the PKCE code verifier generated during the OAuth2 + authorization flow remains valid. + + This duration determines the lifetime of the code verifier cookie, which is exchanged + for the access token when the provider redirects back to the callback endpoint. + + Both this cookie and the CSRF cookie are only consumed on the callback endpoint, so an + authorization flow that is started but never completed leaves them behind until they + expire. Lowering this value along with csrfTokenTTL bounds how many such cookies a + browser can accumulate. It must stay long enough for a user to complete the login, + including any multi-factor prompt: once it elapses the browser drops the cookie and + the callback fails. + + If omitted, Envoy Gateway defaults the code verifier expiration to 10 minutes. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string cookieConfig: description: |- CookieConfigs allows setting the SameSite attribute for OIDC cookies. 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 65d058f7bb..a73c858869 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 @@ -5781,6 +5781,24 @@ spec: required: - name type: object + codeVerifierTTL: + description: |- + CodeVerifierTTL defines how long the PKCE code verifier generated during the OAuth2 + authorization flow remains valid. + + This duration determines the lifetime of the code verifier cookie, which is exchanged + for the access token when the provider redirects back to the callback endpoint. + + Both this cookie and the CSRF cookie are only consumed on the callback endpoint, so an + authorization flow that is started but never completed leaves them behind until they + expire. Lowering this value along with csrfTokenTTL bounds how many such cookies a + browser can accumulate. It must stay long enough for a user to complete the login, + including any multi-factor prompt: once it elapses the browser drops the cookie and + the callback fails. + + If omitted, Envoy Gateway defaults the code verifier expiration to 10 minutes. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string cookieConfig: description: |- CookieConfigs allows setting the SameSite attribute for OIDC cookies. diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 92a0b89813..ed10b16bc1 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -2202,6 +2202,14 @@ func (t *Translator) buildOIDC( } } + if oidc.CodeVerifierTTL != nil { + if d, err := time.ParseDuration(string(*oidc.CodeVerifierTTL)); err == nil { + irOIDC.CodeVerifierTTL = ir.MetaV1DurationPtr(d) + } else { + return nil, fmt.Errorf("invalid codeVerifierTTL: %w", err) + } + } + return irOIDC, nil } diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml index a60dc78e92..2eabd29279 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml @@ -126,6 +126,7 @@ securityPolicies: refreshToken: true defaultRefreshTokenTTL: 24h csrfTokenTTL: 35m + codeVerifierTTL: 2m - apiVersion: gateway.envoyproxy.io/v1alpha1 kind: SecurityPolicy metadata: diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml index 9396f9508a..6918bb9d78 100644 --- a/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml +++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml @@ -281,6 +281,7 @@ securityPolicies: clientID: client1.apps.googleusercontent.com clientSecret: name: client1-secret + codeVerifierTTL: 2m csrfTokenTTL: 35m defaultRefreshTokenTTL: 24h defaultTokenTTL: 30m @@ -451,6 +452,7 @@ xdsIR: oidc: clientID: client1.apps.googleusercontent.com clientSecret: '[redacted]' + codeVerifierTTL: 2m0s cookieSuffix: b0a1b740 csrfTokenTTL: 35m0s defaultRefreshTokenTTL: 24h0m0s diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 0cf1778775..4284f15563 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -1460,6 +1460,9 @@ type OIDC struct { // CSRFTokenTTL configures the lifetime of the csrf token Envoy stores in the cookie. CSRFTokenTTL *metav1.Duration `json:"csrfTokenTTL,omitempty"` + // CodeVerifierTTL configures the lifetime of the PKCE code verifier Envoy stores in the cookie. + CodeVerifierTTL *metav1.Duration `json:"codeVerifierTTL,omitempty"` + // CookieSuffix will be added to the name of the cookies set by the oauth filter. // Adding a suffix avoids multiple oauth filters from overwriting each other's cookies. // These cookies are set by the oauth filter, including: AccessToken, diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index f71396097a..b3e45cb00d 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -3408,6 +3408,11 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { *out = new(metav1.Duration) **out = **in } + if in.CodeVerifierTTL != nil { + in, out := &in.CodeVerifierTTL, &out.CodeVerifierTTL + *out = new(metav1.Duration) + **out = **in + } if in.CookieNameOverrides != nil { in, out := &in.CookieNameOverrides, &out.CookieNameOverrides *out = new(v1alpha1.OIDCCookieNames) diff --git a/internal/xds/translator/oidc.go b/internal/xds/translator/oidc.go index fba5f7da53..b7b233a542 100644 --- a/internal/xds/translator/oidc.go +++ b/internal/xds/translator/oidc.go @@ -229,6 +229,10 @@ func oauth2Config(securityFeatures *ir.SecurityFeatures) (*oauth2v3.OAuth2PerRou oauth2.Config.CsrfTokenExpiresIn = durationpb.New(oidc.CSRFTokenTTL.Duration) } + if oidc.CodeVerifierTTL != nil { + oauth2.Config.CodeVerifierTokenExpiresIn = durationpb.New(oidc.CodeVerifierTTL.Duration) + } + if oidc.ForwardIDTokenHeader != nil { oauth2.Config.ForwardIdToken = &oauth2v3.OAuth2TokenForwarding{ Header: *oidc.ForwardIDTokenHeader, diff --git a/internal/xds/translator/testdata/in/xds-ir/oidc.yaml b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml index 24adbb27c3..c5d6f4f007 100644 --- a/internal/xds/translator/testdata/in/xds-ir/oidc.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml @@ -46,6 +46,7 @@ http: refreshToken: true defaultRefreshTokenTTL: 48h csrfTokenTTL: 35m + codeVerifierTTL: 2m - name: "second-route" hostname: "*" pathMatch: 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 c5900c53f7..65b6e97578 100644 --- a/internal/xds/translator/testdata/out/xds-ir/oidc.routes.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/oidc.routes.yaml @@ -22,6 +22,7 @@ - profile authType: BASIC_AUTH authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth + codeVerifierTokenExpiresIn: 120s credentials: clientId: client.oauth.foo.com cookieNames: diff --git a/release-notes/current/new_features/9645-oidc-code-verifier-ttl.md b/release-notes/current/new_features/9645-oidc-code-verifier-ttl.md new file mode 100644 index 0000000000..44fd1d6391 --- /dev/null +++ b/release-notes/current/new_features/9645-oidc-code-verifier-ttl.md @@ -0,0 +1,3 @@ +Added `spec.oidc.codeVerifierTTL` to SecurityPolicy to configure how long the PKCE code verifier cookie +generated during the OAuth2 authorization flow remains valid. Alongside `csrfTokenTTL`, this bounds how +long the cookies of an abandoned authorization flow live in the browser. Defaults to 10 minutes. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 20d3157df6..fff519a0c5 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -4404,6 +4404,7 @@ _Appears in:_ | `refreshToken` | _boolean_ | false | true | RefreshToken indicates whether the Envoy should automatically refresh the
id token and access token when they expire.
When set to true, the Envoy will use the refresh token to get a new id token
and access token when they expire.
If not specified, defaults to true. | | `defaultRefreshTokenTTL` | _[Duration](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#duration)_ | false | | DefaultRefreshTokenTTL is the default lifetime of the refresh token.
This field is only used when the exp (expiration time) claim is omitted in
the refresh token or the refresh token is not JWT.
If not specified, defaults to 604800s (one week).
Note: this field is only applicable when the "refreshToken" field is set to true. | | `csrfTokenTTL` | _[Duration](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#duration)_ | false | | CSRFTokenTTL defines how long the CSRF token generated during the OAuth2 authorization flow remains valid.
This duration determines the lifetime of the CSRF cookie, which is validated against the CSRF token
in the "state" parameter when the provider redirects back to the callback endpoint.
If omitted, Envoy Gateway defaults the token expiration to 10 minutes. | +| `codeVerifierTTL` | _[Duration](https://gateway-api.sigs.k8s.io/reference/api-spec/1.5/spec/#duration)_ | false | | CodeVerifierTTL defines how long the PKCE code verifier generated during the OAuth2
authorization flow remains valid.
This duration determines the lifetime of the code verifier cookie, which is exchanged
for the access token when the provider redirects back to the callback endpoint.
Both this cookie and the CSRF cookie are only consumed on the callback endpoint, so an
authorization flow that is started but never completed leaves them behind until they
expire. Lowering this value along with csrfTokenTTL bounds how many such cookies a
browser can accumulate. It must stay long enough for a user to complete the login,
including any multi-factor prompt: once it elapses the browser drops the cookie and
the callback fails.
If omitted, Envoy Gateway defaults the code verifier expiration to 10 minutes. | | `disableTokenEncryption` | _boolean_ | false | | Disable token encryption. When set to true, both the access token and the ID token will be stored in plain text.
This option should only be used in secure environments where token encryption is not required.
Default is false (tokens are encrypted). | | `passThroughAuthHeader` | _boolean_ | false | | Skips OIDC authentication when the request contains a header that will be extracted by the JWT filter. Unless
explicitly stated otherwise in the extractFrom field, this will be the "Authorization: Bearer ..." header.
The passThroughAuthHeader option is typically used for non-browser clients that may not be able to handle OIDC
redirects and wish to directly supply a token instead.
If not specified, defaults to false. | diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml index e8ae71f7e4..916c716192 100644 --- a/test/helm/gateway-crds-helm/all.out.yaml +++ b/test/helm/gateway-crds-helm/all.out.yaml @@ -58674,6 +58674,24 @@ spec: required: - name type: object + codeVerifierTTL: + description: |- + CodeVerifierTTL defines how long the PKCE code verifier generated during the OAuth2 + authorization flow remains valid. + + This duration determines the lifetime of the code verifier cookie, which is exchanged + for the access token when the provider redirects back to the callback endpoint. + + Both this cookie and the CSRF cookie are only consumed on the callback endpoint, so an + authorization flow that is started but never completed leaves them behind until they + expire. Lowering this value along with csrfTokenTTL bounds how many such cookies a + browser can accumulate. It must stay long enough for a user to complete the login, + including any multi-factor prompt: once it elapses the browser drops the cookie and + the callback fails. + + If omitted, Envoy Gateway defaults the code verifier expiration to 10 minutes. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string cookieConfig: description: |- CookieConfigs allows setting the SameSite attribute for OIDC cookies. diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml index 8d51ce19a8..dbb7ce6e12 100644 --- a/test/helm/gateway-crds-helm/e2e.out.yaml +++ b/test/helm/gateway-crds-helm/e2e.out.yaml @@ -34612,6 +34612,24 @@ spec: required: - name type: object + codeVerifierTTL: + description: |- + CodeVerifierTTL defines how long the PKCE code verifier generated during the OAuth2 + authorization flow remains valid. + + This duration determines the lifetime of the code verifier cookie, which is exchanged + for the access token when the provider redirects back to the callback endpoint. + + Both this cookie and the CSRF cookie are only consumed on the callback endpoint, so an + authorization flow that is started but never completed leaves them behind until they + expire. Lowering this value along with csrfTokenTTL bounds how many such cookies a + browser can accumulate. It must stay long enough for a user to complete the login, + including any multi-factor prompt: once it elapses the browser drops the cookie and + the callback fails. + + If omitted, Envoy Gateway defaults the code verifier expiration to 10 minutes. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string cookieConfig: description: |- CookieConfigs allows setting the SameSite attribute for OIDC cookies. 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 a8dccebf69..6951e12d1e 100644 --- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml +++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml @@ -34612,6 +34612,24 @@ spec: required: - name type: object + codeVerifierTTL: + description: |- + CodeVerifierTTL defines how long the PKCE code verifier generated during the OAuth2 + authorization flow remains valid. + + This duration determines the lifetime of the code verifier cookie, which is exchanged + for the access token when the provider redirects back to the callback endpoint. + + Both this cookie and the CSRF cookie are only consumed on the callback endpoint, so an + authorization flow that is started but never completed leaves them behind until they + expire. Lowering this value along with csrfTokenTTL bounds how many such cookies a + browser can accumulate. It must stay long enough for a user to complete the login, + including any multi-factor prompt: once it elapses the browser drops the cookie and + the callback fails. + + If omitted, Envoy Gateway defaults the code verifier expiration to 10 minutes. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string cookieConfig: description: |- CookieConfigs allows setting the SameSite attribute for OIDC cookies.