diff --git a/api/v1alpha1/oidc_types.go b/api/v1alpha1/oidc_types.go index ecce7957627..b1de7097f86 100644 --- a/api/v1alpha1/oidc_types.go +++ b/api/v1alpha1/oidc_types.go @@ -30,6 +30,12 @@ type OIDC struct { // +kubebuilder:validation:Required ClientSecret gwapiv1b1.SecretObjectReference `json:"clientSecret"` + // The optional cookie name overrides to be used for Bearer and IdToken cookies in the + // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + // If not specified, uses a randomly generated suffix + // +optional + CookieNames *OIDCCookieNames `json:"cookieNames,omitempty"` + // The OIDC scopes to be used in the // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). // The "openid" scope is always added to the list of scopes if not already @@ -75,3 +81,17 @@ type OIDCProvider struct { // +optional TokenEndpoint *string `json:"tokenEndpoint,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 + // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + // If not specified, defaults to "AccessToken-(randomly generated uid)" + // +optional + AccessToken *string `json:"accessToken,omitempty"` + // The name of the cookie used to store the IdToken in the + // [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + // If not specified, defaults to "IdToken-(randomly generated uid)" + // +optional + IDToken *string `json:"idToken,omitempty"` +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 67918dabdfb..0aee5299921 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -3208,6 +3208,11 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { *out = *in in.Provider.DeepCopyInto(&out.Provider) in.ClientSecret.DeepCopyInto(&out.ClientSecret) + if in.CookieNames != nil { + in, out := &in.CookieNames, &out.CookieNames + *out = new(OIDCCookieNames) + (*in).DeepCopyInto(*out) + } if in.Scopes != nil { in, out := &in.Scopes, &out.Scopes *out = make([]string, len(*in)) @@ -3240,6 +3245,31 @@ func (in *OIDC) DeepCopy() *OIDC { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCCookieNames) DeepCopyInto(out *OIDCCookieNames) { + *out = *in + if in.AccessToken != nil { + in, out := &in.AccessToken, &out.AccessToken + *out = new(string) + **out = **in + } + if in.IDToken != nil { + in, out := &in.IDToken, &out.IDToken + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCCookieNames. +func (in *OIDCCookieNames) DeepCopy() *OIDCCookieNames { + if in == nil { + return nil + } + out := new(OIDCCookieNames) + 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-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml index 00e1b791e68..b5f9a4956ca 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -849,6 +849,25 @@ spec: required: - name type: object + cookieNames: + description: |- + The optional cookie name overrides to be used for Bearer and IdToken cookies in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, uses a randomly generated suffix + properties: + accessToken: + description: |- + The name of the cookie used to store the AccessToken in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, defaults to "AccessToken-(randomly generated uid)" + type: string + idToken: + description: |- + The name of the cookie used to store the IdToken in the + [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). + If not specified, defaults to "IdToken-(randomly generated uid)" + type: string + type: object logoutPath: description: |- The path to log a user out, clearing their credential cookies. diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 29305d067d5..2ba9ea061ba 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -589,7 +589,9 @@ func (t *Translator) buildOIDC( logoutPath = *oidc.LogoutPath } - // Generate a unique cookie suffix for oauth filters + // Generate a unique cookie suffix for oauth filters. + // This is to avoid cookie name collision when multiple security policies are applied + // to the same route. suffix := utils.Digest32(string(policy.UID)) // Get the HMAC secret. @@ -607,17 +609,18 @@ func (t *Translator) buildOIDC( } return &ir.OIDC{ - Name: irConfigName(policy), - Provider: *provider, - ClientID: oidc.ClientID, - ClientSecret: clientSecretBytes, - Scopes: scopes, - Resources: oidc.Resources, - RedirectURL: redirectURL, - RedirectPath: redirectPath, - LogoutPath: logoutPath, - CookieSuffix: suffix, - HMACSecret: hmacData, + Name: irConfigName(policy), + Provider: *provider, + ClientID: oidc.ClientID, + ClientSecret: clientSecretBytes, + Scopes: scopes, + Resources: oidc.Resources, + RedirectURL: redirectURL, + RedirectPath: redirectPath, + LogoutPath: logoutPath, + CookieSuffix: suffix, + CookieNameOverrides: policy.Spec.OIDC.CookieNames, + HMACSecret: hmacData, }, nil } diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-custom-cookies.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-custom-cookies.in.yaml new file mode 100644 index 00000000000..13ce562de3b --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-custom-cookies.in.yaml @@ -0,0 +1,73 @@ +secrets: +- apiVersion: v1 + kind: Secret + metadata: + namespace: envoy-gateway + name: client1-secret + data: + client-secret: Y2xpZW50MTpzZWNyZXQK +- apiVersion: v1 + kind: Secret + metadata: + namespace: envoy-gateway-system + name: envoy-oidc-hmac + data: + hmac-secret: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY= +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - www.example.com + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/foo" + backendRefs: + - name: service-1 + port: 8080 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway # This policy should attach httproute-2 + uid: b8284d0f-de82-4c65-b204-96a0d3f258a1 + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + oidc: + provider: + issuer: "https://accounts.google.com" + clientID: "client1.apps.googleusercontent.com" + clientSecret: + name: "client1-secret" + redirectURL: "https://www.example.com/bar/oauth2/callback" + logoutPath: "/bar/logout" + cookieNames: + idToken: "CustomIdTokenCookie" + accessToken: "CustomAccessTokenCookie" diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-custom-cookies.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-custom-cookies.out.yaml new file mode 100755 index 00000000000..07ceea24fae --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-custom-cookies.out.yaml @@ -0,0 +1,186 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - www.example.com + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: /foo + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: null + name: envoy-gateway/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + uid: b8284d0f-de82-4c65-b204-96a0d3f258a1 + spec: + oidc: + clientID: client1.apps.googleusercontent.com + clientSecret: + group: null + kind: null + name: client1-secret + cookieNames: + accessToken: CustomAccessTokenCookie + idToken: CustomIdTokenCookie + logoutPath: /bar/logout + provider: + issuer: https://accounts.google.com + redirectURL: https://www.example.com/bar/oauth2/callback + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: www.example.com + isHTTP2: false + name: httproute/default/httproute-1/rule/0/match/0/www_example_com + pathMatch: + distinct: false + name: "" + prefix: /foo + security: + oidc: + clientID: client1.apps.googleusercontent.com + clientSecret: Y2xpZW50MTpzZWNyZXQK + cookieNameOverrides: + accessToken: CustomAccessTokenCookie + idToken: CustomIdTokenCookie + cookieSuffix: b0a1b740 + hmacSecret: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY= + logoutPath: /bar/logout + name: securitypolicy/envoy-gateway/policy-for-gateway + provider: + authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth + tokenEndpoint: https://oauth2.googleapis.com/token + redirectPath: /bar/oauth2/callback + redirectURL: https://www.example.com/bar/oauth2/callback + scopes: + - openid diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 77505d57561..1fbebde2442 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -686,9 +686,12 @@ type OIDC struct { // 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: BearerToken, + // These cookies are set by the oauth filter, including: AccessToken, // OauthHMAC, OauthExpires, IdToken, and RefreshToken. CookieSuffix string `json:"cookieSuffix,omitempty"` + + // CookieNameOverrides can optionally override the generated name of the cookies set by the oauth filter. + CookieNameOverrides *egv1a1.OIDCCookieNames `json:"cookieNameOverrides,omitempty"` } type OIDCProvider struct { diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 7d93ad057ff..94aca3ecc26 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -1492,6 +1492,11 @@ func (in *OIDC) DeepCopyInto(out *OIDC) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.CookieNameOverrides != nil { + in, out := &in.CookieNameOverrides, &out.CookieNameOverrides + *out = new(v1alpha1.OIDCCookieNames) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDC. diff --git a/internal/xds/translator/oidc.go b/internal/xds/translator/oidc.go index 183fb7944aa..ae54603f57b 100644 --- a/internal/xds/translator/oidc.go +++ b/internal/xds/translator/oidc.go @@ -159,7 +159,7 @@ func oauth2Config(oidc *ir.OIDC) (*oauth2v3.OAuth2, error) { }, }, CookieNames: &oauth2v3.OAuth2Credentials_CookieNames{ - BearerToken: fmt.Sprintf("BearerToken-%s", oidc.CookieSuffix), + BearerToken: fmt.Sprintf("AccessToken-%s", oidc.CookieSuffix), OauthHmac: fmt.Sprintf("OauthHMAC-%s", oidc.CookieSuffix), OauthExpires: fmt.Sprintf("OauthExpires-%s", oidc.CookieSuffix), IdToken: fmt.Sprintf("IdToken-%s", oidc.CookieSuffix), @@ -172,6 +172,17 @@ func oauth2Config(oidc *ir.OIDC) (*oauth2v3.OAuth2, error) { Resources: oidc.Resources, }, } + + if oidc.CookieNameOverrides != nil && + oidc.CookieNameOverrides.AccessToken != nil { + oauth2.Config.Credentials.CookieNames.BearerToken = *oidc.CookieNameOverrides.AccessToken + } + + if oidc.CookieNameOverrides != nil && + oidc.CookieNameOverrides.IDToken != nil { + oauth2.Config.Credentials.CookieNames.IdToken = *oidc.CookieNameOverrides.IDToken + } + return oauth2, nil } diff --git a/internal/xds/translator/testdata/in/xds-ir/oidc.yaml b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml index b2e395f8320..68e36bffab1 100644 --- a/internal/xds/translator/testdata/in/xds-ir/oidc.yaml +++ b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml @@ -66,3 +66,6 @@ http: redirectPath: "/bar/oauth2/callback" logoutPath: "/bar/logout" cookieSuffix: 5f93c2e4 + cookieNameOverrides: + idToken: "CustomIdTokenOverride" + accessToken: "CustomAccessTokenOverride" diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml index 1365c290cb7..a06b7265316 100755 --- a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port-with-different-filters.listeners.yaml @@ -117,7 +117,7 @@ credentials: clientId: client.oauth.foo.com cookieNames: - bearerToken: BearerToken-5F93C2E4 + bearerToken: AccessToken-5F93C2E4 idToken: IdToken-5F93C2E4 oauthExpires: OauthExpires-5F93C2E4 oauthHmac: OauthHMAC-5F93C2E4 diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml index 95e075f047e..ab8556ba154 100644 --- a/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml +++ b/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml @@ -28,7 +28,7 @@ credentials: clientId: client.oauth.foo.com cookieNames: - bearerToken: BearerToken-5F93C2E4 + bearerToken: AccessToken-5F93C2E4 idToken: IdToken-5F93C2E4 oauthExpires: OauthExpires-5F93C2E4 oauthHmac: OauthHMAC-5F93C2E4 @@ -71,8 +71,8 @@ credentials: clientId: client.oauth.bar.com cookieNames: - bearerToken: BearerToken-5f93c2e4 - idToken: IdToken-5f93c2e4 + bearerToken: CustomAccessTokenOverride + idToken: CustomIdTokenOverride oauthExpires: OauthExpires-5f93c2e4 oauthHmac: OauthHMAC-5f93c2e4 refreshToken: RefreshToken-5f93c2e4 diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index d28b28339dd..c9bbefd18cc 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -2313,12 +2313,28 @@ _Appears in:_ | `provider` | _[OIDCProvider](#oidcprovider)_ | true | The OIDC Provider configuration. | | `clientID` | _string_ | true | The client ID to be used in the OIDC
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). | | `clientSecret` | _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | true | The Kubernetes secret which contains the OIDC client secret to be used in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).

This is an Opaque secret. The client secret should be stored in the key
"client-secret". | +| `cookieNames` | _[OIDCCookieNames](#oidccookienames)_ | false | The optional cookie name overrides to be used for Bearer and IdToken cookies in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, uses a randomly generated suffix | | `scopes` | _string array_ | false | The OIDC scopes to be used in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
The "openid" scope is always added to the list of scopes if not already
specified. | | `resources` | _string array_ | false | The OIDC resources to be used in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). | | `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" | | `logoutPath` | _string_ | true | The path to log a user out, clearing their credential cookies.
If not specified, uses a default logout path "/logout" | +#### OIDCCookieNames + + + +OIDCCookieNames defines the names of cookies to use in the Envoy OIDC filter. + +_Appears in:_ +- [OIDC](#oidc) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `accessToken` | _string_ | false | The name of the cookie used to store the AccessToken in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, defaults to "AccessToken-(randomly generated uid)" | +| `idToken` | _string_ | false | The name of the cookie used to store the IdToken in the
[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
If not specified, defaults to "IdToken-(randomly generated uid)" | + + #### OIDCProvider