From 125fb3413f6241839bd96868490ff5416515d7ba Mon Sep 17 00:00:00 2001 From: David Gogl <1381862+kengou@users.noreply.github.com> Date: Mon, 27 Oct 2025 21:45:57 +0100 Subject: [PATCH 01/12] feat(dex): add option to modify user id claim, skip email_verified via toggle --- api/v1alpha1/organization_types.go | 7 ++++++ .../crds/greenhouse.sap_organizations.yaml | 10 +++++++++ docs/reference/api/index.html | 22 +++++++++++++++++++ docs/reference/api/openapi.yaml | 9 ++++++++ internal/controller/organization/dex.go | 17 +++++++------- types/typescript/schema.d.ts | 7 ++++++ 6 files changed, 64 insertions(+), 8 deletions(-) diff --git a/api/v1alpha1/organization_types.go b/api/v1alpha1/organization_types.go index c5447a819..5f2c67ccd 100644 --- a/api/v1alpha1/organization_types.go +++ b/api/v1alpha1/organization_types.go @@ -83,6 +83,13 @@ type OIDCConfig struct { // OAuth2ClientRedirectURIs are a registered set of redirect URIs. When redirecting from the idproxy to // the client application, the URI requested to redirect to must be contained in this list. OAuth2ClientRedirectURIs []string `json:"oauth2ClientRedirectURIs,omitempty"` + // InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. + // +kubebuilder:validation:default:=false + // +kubebuilder:validation:Enum:=true;false + InsecureSkipEmailVerified bool `json:"insecureSkipEmailVerified,omitempty"` + // UserIDClaim is the claim to be used as user ID. + // +kubebuilder:validation:default:="login_name" + UserIDClaim string `json:"userIDClaim,omitempty"` } type SCIMConfig struct { diff --git a/charts/manager/crds/greenhouse.sap_organizations.yaml b/charts/manager/crds/greenhouse.sap_organizations.yaml index 3885c27b1..2ad4c073b 100644 --- a/charts/manager/crds/greenhouse.sap_organizations.yaml +++ b/charts/manager/crds/greenhouse.sap_organizations.yaml @@ -92,6 +92,13 @@ spec: - key - name type: object + insecureSkipEmailVerified: + description: InsecureSkipEmailVerified allows to skip the + verification of the "email_verified" claim in ID tokens. + enum: + - true + - false + type: boolean issuer: description: Issuer is the URL of the identity service. type: string @@ -107,6 +114,9 @@ spec: RedirectURI is the redirect URI to be used for the OIDC flow against the upstream IdP. If none is specified, the Greenhouse ID proxy will be used. type: string + userIDClaim: + description: UserIDClaim is the claim to be used as user ID. + type: string required: - clientIDReference - clientSecretReference diff --git a/docs/reference/api/index.html b/docs/reference/api/index.html index 99a1e1d75..2675a919c 100644 --- a/docs/reference/api/index.html +++ b/docs/reference/api/index.html @@ -1807,6 +1807,28 @@

OIDCConfig the client application, the URI requested to redirect to must be contained in this list.

+ + +insecureSkipEmailVerified
+ +bool + + + +

InsecureSkipEmailVerified allows to skip the verification of the “email_verified” claim in ID tokens.

+ + + + +userIDClaim
+ +string + + + +

UserIDClaim is the claim to be used as user ID.

+ + diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index d8deb48e3..e65313185 100755 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -669,6 +669,12 @@ components: - key - name type: object + insecureSkipEmailVerified: + description: InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. + enum: + - true + - false + type: boolean issuer: description: Issuer is the URL of the identity service. type: string @@ -684,6 +690,9 @@ components: RedirectURI is the redirect URI to be used for the OIDC flow against the upstream IdP. If none is specified, the Greenhouse ID proxy will be used. type: string + userIDClaim: + description: UserIDClaim is the claim to be used as user ID. + type: string required: - clientIDReference - clientSecretReference diff --git a/internal/controller/organization/dex.go b/internal/controller/organization/dex.go index b679f7193..f09468669 100644 --- a/internal/controller/organization/dex.go +++ b/internal/controller/organization/dex.go @@ -111,14 +111,15 @@ func (r *OrganizationReconciler) reconcileDexConnector(ctx context.Context, org return err } oidcConfig := &oidc.Config{ - Issuer: org.Spec.Authentication.OIDCConfig.Issuer, - ClientID: clientID, - ClientSecret: clientSecret, - RedirectURI: redirectURL, - UserNameKey: "login_name", - UserIDKey: "login_name", - InsecureSkipVerify: true, - InsecureEnableGroups: true, + Issuer: org.Spec.Authentication.OIDCConfig.Issuer, + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURI: redirectURL, + UserNameKey: org.Spec.Authentication.OIDCConfig.UserIDClaim, + UserIDKey: org.Spec.Authentication.OIDCConfig.UserIDClaim, + InsecureSkipEmailVerified: org.Spec.Authentication.OIDCConfig.InsecureSkipEmailVerified, + InsecureSkipVerify: true, + InsecureEnableGroups: true, } configByte, err := json.Marshal(oidcConfig) if err != nil { diff --git a/types/typescript/schema.d.ts b/types/typescript/schema.d.ts index 3daf40c18..64bbca219 100644 --- a/types/typescript/schema.d.ts +++ b/types/typescript/schema.d.ts @@ -533,6 +533,11 @@ export interface components { /** @description Name of the secret in the same namespace. */ name: string; }; + /** + * @description InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. + * @enum {boolean} + */ + insecureSkipEmailVerified?: true | false; /** @description Issuer is the URL of the identity service. */ issuer: string; /** @@ -545,6 +550,8 @@ export interface components { * If none is specified, the Greenhouse ID proxy will be used. */ redirectURI?: string; + /** @description UserIDClaim is the claim to be used as user ID. */ + userIDClaim?: string; }; /** @description SCIMConfig configures the SCIM client. */ scim?: { From 4874534592988cb2010a928ab527433dd892cde0 Mon Sep 17 00:00:00 2001 From: David Gogl <1381862+kengou@users.noreply.github.com> Date: Mon, 27 Oct 2025 21:54:17 +0100 Subject: [PATCH 02/12] fix(api): fix default values --- api/v1alpha1/organization_types.go | 4 ++-- charts/manager/crds/greenhouse.sap_organizations.yaml | 2 ++ docs/reference/api/openapi.yaml | 2 ++ types/typescript/schema.d.ts | 10 +++++++--- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/organization_types.go b/api/v1alpha1/organization_types.go index 5f2c67ccd..ff8177284 100644 --- a/api/v1alpha1/organization_types.go +++ b/api/v1alpha1/organization_types.go @@ -84,11 +84,11 @@ type OIDCConfig struct { // the client application, the URI requested to redirect to must be contained in this list. OAuth2ClientRedirectURIs []string `json:"oauth2ClientRedirectURIs,omitempty"` // InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. - // +kubebuilder:validation:default:=false + // +kubebuilder:default:=false // +kubebuilder:validation:Enum:=true;false InsecureSkipEmailVerified bool `json:"insecureSkipEmailVerified,omitempty"` // UserIDClaim is the claim to be used as user ID. - // +kubebuilder:validation:default:="login_name" + // +kubebuilder:default:="login_name" UserIDClaim string `json:"userIDClaim,omitempty"` } diff --git a/charts/manager/crds/greenhouse.sap_organizations.yaml b/charts/manager/crds/greenhouse.sap_organizations.yaml index 2ad4c073b..e77bd3bd7 100644 --- a/charts/manager/crds/greenhouse.sap_organizations.yaml +++ b/charts/manager/crds/greenhouse.sap_organizations.yaml @@ -93,6 +93,7 @@ spec: - name type: object insecureSkipEmailVerified: + default: false description: InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. enum: @@ -115,6 +116,7 @@ spec: If none is specified, the Greenhouse ID proxy will be used. type: string userIDClaim: + default: login_name description: UserIDClaim is the claim to be used as user ID. type: string required: diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index e65313185..fa5585217 100755 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -670,6 +670,7 @@ components: - name type: object insecureSkipEmailVerified: + default: false description: InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. enum: - true @@ -691,6 +692,7 @@ components: If none is specified, the Greenhouse ID proxy will be used. type: string userIDClaim: + default: login_name description: UserIDClaim is the claim to be used as user ID. type: string required: diff --git a/types/typescript/schema.d.ts b/types/typescript/schema.d.ts index 64bbca219..6d07967c1 100644 --- a/types/typescript/schema.d.ts +++ b/types/typescript/schema.d.ts @@ -535,9 +535,10 @@ export interface components { }; /** * @description InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. + * @default false * @enum {boolean} */ - insecureSkipEmailVerified?: true | false; + insecureSkipEmailVerified: true | false; /** @description Issuer is the URL of the identity service. */ issuer: string; /** @@ -550,8 +551,11 @@ export interface components { * If none is specified, the Greenhouse ID proxy will be used. */ redirectURI?: string; - /** @description UserIDClaim is the claim to be used as user ID. */ - userIDClaim?: string; + /** + * @description UserIDClaim is the claim to be used as user ID. + * @default login_name + */ + userIDClaim: string; }; /** @description SCIMConfig configures the SCIM client. */ scim?: { From f7b847ac1a2aee41153a076862aa64a4b5797127 Mon Sep 17 00:00:00 2001 From: David Gogl <1381862+kengou@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:12:21 +0100 Subject: [PATCH 03/12] update org sample --- config/samples/organization/demo.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/samples/organization/demo.yaml b/config/samples/organization/demo.yaml index 4b02a3df8..6afb30e12 100644 --- a/config/samples/organization/demo.yaml +++ b/config/samples/organization/demo.yaml @@ -53,3 +53,5 @@ spec: name: demo-oidc issuer: https://global.accounts.dev redirectURI: https://bogus.accounts.foo + InsecureSkipEmailVerified: true + UserIDClaim: email From d2320e3ee45c1095e591c23dc6b2f3568f4e9ebe Mon Sep 17 00:00:00 2001 From: David Gogl <1381862+kengou@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:13:29 +0100 Subject: [PATCH 04/12] fix org sample --- config/samples/organization/demo.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/samples/organization/demo.yaml b/config/samples/organization/demo.yaml index 6afb30e12..cac535baf 100644 --- a/config/samples/organization/demo.yaml +++ b/config/samples/organization/demo.yaml @@ -53,5 +53,5 @@ spec: name: demo-oidc issuer: https://global.accounts.dev redirectURI: https://bogus.accounts.foo - InsecureSkipEmailVerified: true - UserIDClaim: email + insecureSkipEmailVerified: true + userIDClaim: email From 7a52ae16d553c04b26f044b01f98ee54c78e4b67 Mon Sep 17 00:00:00 2001 From: David Gogl <1381862+kengou@users.noreply.github.com> Date: Sun, 16 Nov 2025 01:02:34 +0100 Subject: [PATCH 05/12] introduce ExtraConfig --- api/v1alpha1/organization_types.go | 5 +++++ internal/controller/organization/dex.go | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) mode change 100644 => 100755 api/v1alpha1/organization_types.go mode change 100644 => 100755 internal/controller/organization/dex.go diff --git a/api/v1alpha1/organization_types.go b/api/v1alpha1/organization_types.go old mode 100644 new mode 100755 index ff8177284..55cd100c5 --- a/api/v1alpha1/organization_types.go +++ b/api/v1alpha1/organization_types.go @@ -83,6 +83,11 @@ type OIDCConfig struct { // OAuth2ClientRedirectURIs are a registered set of redirect URIs. When redirecting from the idproxy to // the client application, the URI requested to redirect to must be contained in this list. OAuth2ClientRedirectURIs []string `json:"oauth2ClientRedirectURIs,omitempty"` + // ExtraClaims contains additional configuration for extra claims. + ExtraConfig *OIDCExtraConfig `json:"extraConfig,omitempty"` +} + +type OIDCExtraConfig struct { // InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. // +kubebuilder:default:=false // +kubebuilder:validation:Enum:=true;false diff --git a/internal/controller/organization/dex.go b/internal/controller/organization/dex.go old mode 100644 new mode 100755 index f09468669..ff026db68 --- a/internal/controller/organization/dex.go +++ b/internal/controller/organization/dex.go @@ -110,14 +110,24 @@ func (r *OrganizationReconciler) reconcileDexConnector(ctx context.Context, org if err != nil { return err } + var userNameKey = "login_name" + var skipEmailVerified = false + if org.Spec.Authentication.OIDCConfig.ExtraConfig != nil { + if org.Spec.Authentication.OIDCConfig.ExtraConfig.UserIDClaim == "" { + userNameKey = org.Spec.Authentication.OIDCConfig.ExtraConfig.UserIDClaim + } + if org.Spec.Authentication.OIDCConfig.ExtraConfig != nil { + skipEmailVerified = org.Spec.Authentication.OIDCConfig.ExtraConfig.InsecureSkipEmailVerified + } + } oidcConfig := &oidc.Config{ Issuer: org.Spec.Authentication.OIDCConfig.Issuer, ClientID: clientID, ClientSecret: clientSecret, RedirectURI: redirectURL, - UserNameKey: org.Spec.Authentication.OIDCConfig.UserIDClaim, - UserIDKey: org.Spec.Authentication.OIDCConfig.UserIDClaim, - InsecureSkipEmailVerified: org.Spec.Authentication.OIDCConfig.InsecureSkipEmailVerified, + UserNameKey: userNameKey, + UserIDKey: userNameKey, + InsecureSkipEmailVerified: skipEmailVerified, InsecureSkipVerify: true, InsecureEnableGroups: true, } From da900f0f3c2df2f9fc8c0756a50fecba1419c55a Mon Sep 17 00:00:00 2001 From: "cloud-operator-bot[bot]" <224791424+cloud-operator-bot[bot]@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:05:48 +0000 Subject: [PATCH 06/12] Automatic generation of CRD API Docs --- api/v1alpha1/zz_generated.deepcopy.go | 20 +++++++++++ .../crds/greenhouse.sap_organizations.yaml | 31 ++++++++++------- docs/reference/api/index.html | 33 +++++++++++++++++++ docs/reference/api/openapi.yaml | 26 ++++++++------- 4 files changed, 87 insertions(+), 23 deletions(-) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 051db62f4..f27c0596b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -791,6 +791,11 @@ func (in *OIDCConfig) DeepCopyInto(out *OIDCConfig) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.ExtraConfig != nil { + in, out := &in.ExtraConfig, &out.ExtraConfig + *out = new(OIDCExtraConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCConfig. @@ -803,6 +808,21 @@ func (in *OIDCConfig) DeepCopy() *OIDCConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCExtraConfig) DeepCopyInto(out *OIDCExtraConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCExtraConfig. +func (in *OIDCExtraConfig) DeepCopy() *OIDCExtraConfig { + if in == nil { + return nil + } + out := new(OIDCExtraConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Organization) DeepCopyInto(out *Organization) { *out = *in diff --git a/charts/manager/crds/greenhouse.sap_organizations.yaml b/charts/manager/crds/greenhouse.sap_organizations.yaml index e77bd3bd7..f153c6ccc 100644 --- a/charts/manager/crds/greenhouse.sap_organizations.yaml +++ b/charts/manager/crds/greenhouse.sap_organizations.yaml @@ -92,14 +92,25 @@ spec: - key - name type: object - insecureSkipEmailVerified: - default: false - description: InsecureSkipEmailVerified allows to skip the - verification of the "email_verified" claim in ID tokens. - enum: - - true - - false - type: boolean + extraConfig: + description: ExtraClaims contains additional configuration + for extra claims. + properties: + insecureSkipEmailVerified: + default: false + description: InsecureSkipEmailVerified allows to skip + the verification of the "email_verified" claim in ID + tokens. + enum: + - true + - false + type: boolean + userIDClaim: + default: login_name + description: UserIDClaim is the claim to be used as user + ID. + type: string + type: object issuer: description: Issuer is the URL of the identity service. type: string @@ -115,10 +126,6 @@ spec: RedirectURI is the redirect URI to be used for the OIDC flow against the upstream IdP. If none is specified, the Greenhouse ID proxy will be used. type: string - userIDClaim: - default: login_name - description: UserIDClaim is the claim to be used as user ID. - type: string required: - clientIDReference - clientSecretReference diff --git a/docs/reference/api/index.html b/docs/reference/api/index.html index 2675a919c..65adbbefd 100644 --- a/docs/reference/api/index.html +++ b/docs/reference/api/index.html @@ -1809,6 +1809,39 @@

OIDCConfig +extraConfig
+ + +OIDCExtraConfig + + + + +

ExtraClaims contains additional configuration for extra claims.

+ + + + + + +

OIDCExtraConfig +

+

+(Appears on: +OIDCConfig) +

+
+
+ + + + + + + + + + @@ -2032,7 +2032,8 @@

OIDCExtraConfig

@@ -2043,7 +2044,8 @@

OIDCExtraConfig

diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index 7a0e8df1d..88112b48e 100755 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -692,18 +692,19 @@ components: - name type: object extraConfig: - description: ExtraClaims contains additional configuration for extra claims. + description: ExtraConfig contains additional OIDC configuration for claim mapping and token validation behavior. properties: insecureSkipEmailVerified: default: false - description: InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. - enum: - - true - - false + description: |- + InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. + This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations). type: boolean userIDClaim: default: login_name - description: UserIDClaim is the claim to be used as user ID. + description: |- + UserIDClaim is the claim to be used as both user ID and username. + When set, it overrides both UserIDKey and UserNameKey in the dex OIDC connector config. type: string type: object issuer: diff --git a/internal/controller/organization/dex.go b/internal/controller/organization/dex.go index ff026db68..2228d69ec 100755 --- a/internal/controller/organization/dex.go +++ b/internal/controller/organization/dex.go @@ -113,12 +113,10 @@ func (r *OrganizationReconciler) reconcileDexConnector(ctx context.Context, org var userNameKey = "login_name" var skipEmailVerified = false if org.Spec.Authentication.OIDCConfig.ExtraConfig != nil { - if org.Spec.Authentication.OIDCConfig.ExtraConfig.UserIDClaim == "" { + if org.Spec.Authentication.OIDCConfig.ExtraConfig.UserIDClaim != "" { userNameKey = org.Spec.Authentication.OIDCConfig.ExtraConfig.UserIDClaim } - if org.Spec.Authentication.OIDCConfig.ExtraConfig != nil { - skipEmailVerified = org.Spec.Authentication.OIDCConfig.ExtraConfig.InsecureSkipEmailVerified - } + skipEmailVerified = org.Spec.Authentication.OIDCConfig.ExtraConfig.InsecureSkipEmailVerified } oidcConfig := &oidc.Config{ Issuer: org.Spec.Authentication.OIDCConfig.Issuer, diff --git a/internal/controller/organization/organization_controller_test.go b/internal/controller/organization/organization_controller_test.go index e09efb785..ff7d77af6 100644 --- a/internal/controller/organization/organization_controller_test.go +++ b/internal/controller/organization/organization_controller_test.go @@ -4,10 +4,12 @@ package organization_test import ( + "encoding/json" "fmt" "slices" + dexoidc "github.com/dexidp/dex/connector/oidc" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" @@ -390,6 +392,61 @@ var _ = Describe("Test Organization reconciliation", Ordered, func() { } test.EventuallyDeleted(test.Ctx, test.K8sClient, team) }) + + It("should propagate ExtraConfig to dex connector", func() { + team := setup.CreateTeam(test.Ctx, "test-team-extra", test.WithTeamLabel(greenhouseapis.LabelKeySupportGroup, "true")) + + defaultOrg := setup.CreateDefaultOrgWithOIDCSecret(test.Ctx, team.Name) + test.EventuallyCreated(test.Ctx, test.K8sClient, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: defaultOrg.Name}}) + Eventually(func(g Gomega) { + err := test.K8sClient.Get(test.Ctx, types.NamespacedName{Name: defaultOrg.Name}, defaultOrg) + g.Expect(err).ToNot(HaveOccurred()) + oidcCondition := defaultOrg.Status.GetConditionByType(greenhousev1alpha1.OrganizationOICDConfigured) + g.Expect(oidcCondition).ToNot(BeNil()) + g.Expect(oidcCondition.IsTrue()).To(BeTrue()) + }).Should(Succeed()) + + By("creating a test organization with ExtraConfig") + extraConfigOrg := setup.CreateOrganization(test.Ctx, "test-extraconfig-org", test.WithMappedAdminIDPGroup("EXTRA_CONFIG_GROUP")) + test.EventuallyCreated(test.Ctx, test.K8sClient, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: extraConfigOrg.Name}}) + + oidcSecret := setup.CreateOrgOIDCSecret(test.Ctx, extraConfigOrg.Name, team.Name) + extraConfigOrg = setup.UpdateOrganization(test.Ctx, + extraConfigOrg.Name, + test.WithOIDCConfig(test.OIDCIssuer, oidcSecret.Name, test.OIDCClientIDKey, test.OIDCClientSecretKey), + test.WithOIDCExtraConfig("email", true), + ) + + By("checking Organization OIDC status is ready") + Eventually(func(g Gomega) { + err := test.K8sClient.Get(test.Ctx, types.NamespacedName{Name: extraConfigOrg.Name}, extraConfigOrg) + g.Expect(err).ToNot(HaveOccurred()) + oidcCondition := extraConfigOrg.Status.GetConditionByType(greenhousev1alpha1.OrganizationOICDConfigured) + g.Expect(oidcCondition).ToNot(BeNil(), "OrganizationOICDConfigured should be set") + g.Expect(oidcCondition.IsTrue()).To(BeTrue(), "OrganizationOICDConfigured should be True") + }).Should(Succeed()) + + if DexStorageType == dex.K8s { + By("verifying the dex connector config contains ExtraConfig values") + connectors := &dexapi.ConnectorList{} + err := setup.List(test.Ctx, connectors) + Expect(err).ToNot(HaveOccurred()) + + filteredConnectors := slices.DeleteFunc(connectors.Items, func(c dexapi.Connector) bool { + return c.ID != extraConfigOrg.Name + }) + Expect(filteredConnectors).To(HaveLen(1), "there should be exactly one connector for the org") + + var connectorConfig dexoidc.Config + Expect(json.Unmarshal(filteredConnectors[0].Config, &connectorConfig)).To(Succeed(), "connector config should be valid JSON") + Expect(connectorConfig.UserNameKey).To(Equal("email"), "UserNameKey should be set to the custom claim") + Expect(connectorConfig.UserIDKey).To(Equal("email"), "UserIDKey should be set to the custom claim") + Expect(connectorConfig.InsecureSkipEmailVerified).To(BeTrue(), "InsecureSkipEmailVerified should be true") + } + + test.EventuallyDeleted(test.Ctx, test.K8sClient, &greenhousev1alpha1.Organization{ObjectMeta: metav1.ObjectMeta{Name: extraConfigOrg.Name}}) + test.EventuallyDeleted(test.Ctx, test.K8sClient, team) + }) }) When("reconciling PluginDefinitionCatalog ServiceAccount for regular organization", Ordered, func() { diff --git a/internal/test/resources.go b/internal/test/resources.go index 34564cae4..cda6d8070 100644 --- a/internal/test/resources.go +++ b/internal/test/resources.go @@ -134,6 +134,23 @@ func WithOIDCConfig(issuer, secretName, clientIDKey, clientSecretKey string) fun } } +// WithOIDCExtraConfig sets the OIDCExtraConfig on an Organization's OIDCConfig. +// Must be used after WithOIDCConfig to ensure OIDCConfig is initialized. +func WithOIDCExtraConfig(userIDClaim string, insecureSkipEmailVerified bool) func(*greenhousev1alpha1.Organization) { + return func(org *greenhousev1alpha1.Organization) { + if org.Spec.Authentication == nil { + org.Spec.Authentication = &greenhousev1alpha1.Authentication{} + } + if org.Spec.Authentication.OIDCConfig == nil { + org.Spec.Authentication.OIDCConfig = &greenhousev1alpha1.OIDCConfig{} + } + org.Spec.Authentication.OIDCConfig.ExtraConfig = &greenhousev1alpha1.OIDCExtraConfig{ + UserIDClaim: userIDClaim, + InsecureSkipEmailVerified: insecureSkipEmailVerified, + } + } +} + // NewOrganization returns a greenhousev1alpha1.Organization object. Opts can be used to set the desired state of the Organization. func NewOrganization(ctx context.Context, name string, opts ...func(*greenhousev1alpha1.Organization)) *greenhousev1alpha1.Organization { org := &greenhousev1alpha1.Organization{ diff --git a/types/typescript/schema.d.ts b/types/typescript/schema.d.ts index 1110773fb..8952bb1e1 100644 --- a/types/typescript/schema.d.ts +++ b/types/typescript/schema.d.ts @@ -458,15 +458,9 @@ export interface components { /** @description Name of the node. */ name: string; }[]; - /** - * Format: int32 - * @description ReadyNodes represent the number of ready nodes in the cluster. - */ + /** @description ReadyNodes represent the number of ready nodes in the cluster. */ ready?: number; - /** - * Format: int32 - * @description Total represent the number of all the nodes in the cluster. - */ + /** @description Total represent the number of all the nodes in the cluster. */ total?: number; }; /** @description StatusConditions contain the different conditions that constitute the status of the Cluster. */ @@ -546,12 +540,21 @@ export interface components { /** @description Name of the secret in the same namespace. */ name: string; }; - /** - * @description InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. - * @default false - * @enum {boolean} - */ - insecureSkipEmailVerified: true | false; + /** @description ExtraConfig contains additional OIDC configuration for claim mapping and token validation behavior. */ + extraConfig?: { + /** + * @description InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. + * This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations). + * @default false + */ + insecureSkipEmailVerified: boolean; + /** + * @description UserIDClaim is the claim to be used as both user ID and username. + * When set, it overrides both UserIDKey and UserNameKey in the dex OIDC connector config. + * @default login_name + */ + userIDClaim: string; + }; /** @description Issuer is the URL of the identity service. */ issuer: string; /** @@ -564,11 +567,6 @@ export interface components { * If none is specified, the Greenhouse ID proxy will be used. */ redirectURI?: string; - /** - * @description UserIDClaim is the claim to be used as user ID. - * @default login_name - */ - userIDClaim: string; }; /** @description SCIMConfig configures the SCIM client. */ scim?: { @@ -1467,8 +1465,13 @@ export interface components { * If empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace. */ namespaces?: string[]; - /** @description TeamRef references a Greenhouse Team by name */ + /** + * @description Deprecated: Use TeamRefs instead. + * TeamRef references a Greenhouse Team by name + */ teamRef?: string; + /** @description TeamRefs references Greenhouse Teams by name */ + teamRefs?: string[]; /** @description TeamRoleRef references a Greenhouse TeamRole by name */ teamRoleRef?: string; /** @description Usernames defines list of users to add to the (Cluster-)RoleBindings */ From acb80d326590a8739c26d130b8c09c1522614be3 Mon Sep 17 00:00:00 2001 From: "cloud-operator-bot[bot]" <224791424+cloud-operator-bot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 09:49:47 +0000 Subject: [PATCH 10/12] Automatic generation of CRD API Docs --- charts/manager/crds/greenhouse.sap_catalogs.yaml | 2 +- charts/manager/crds/greenhouse.sap_clusterkubeconfigs.yaml | 2 +- .../manager/crds/greenhouse.sap_clusterplugindefinitions.yaml | 2 +- charts/manager/crds/greenhouse.sap_clusters.yaml | 2 +- charts/manager/crds/greenhouse.sap_organizations.yaml | 2 +- charts/manager/crds/greenhouse.sap_plugindefinitions.yaml | 2 +- charts/manager/crds/greenhouse.sap_pluginpresets.yaml | 2 +- charts/manager/crds/greenhouse.sap_plugins.yaml | 2 +- charts/manager/crds/greenhouse.sap_teamrolebindings.yaml | 2 +- charts/manager/crds/greenhouse.sap_teamroles.yaml | 2 +- charts/manager/crds/greenhouse.sap_teams.yaml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/charts/manager/crds/greenhouse.sap_catalogs.yaml b/charts/manager/crds/greenhouse.sap_catalogs.yaml index 48bf3de2a..93ba9aeb7 100644 --- a/charts/manager/crds/greenhouse.sap_catalogs.yaml +++ b/charts/manager/crds/greenhouse.sap_catalogs.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: catalogs.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_clusterkubeconfigs.yaml b/charts/manager/crds/greenhouse.sap_clusterkubeconfigs.yaml index b3850d16a..80ee964aa 100644 --- a/charts/manager/crds/greenhouse.sap_clusterkubeconfigs.yaml +++ b/charts/manager/crds/greenhouse.sap_clusterkubeconfigs.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: clusterkubeconfigs.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_clusterplugindefinitions.yaml b/charts/manager/crds/greenhouse.sap_clusterplugindefinitions.yaml index 1d2934629..ce044ba36 100644 --- a/charts/manager/crds/greenhouse.sap_clusterplugindefinitions.yaml +++ b/charts/manager/crds/greenhouse.sap_clusterplugindefinitions.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: clusterplugindefinitions.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_clusters.yaml b/charts/manager/crds/greenhouse.sap_clusters.yaml index 285292d74..48abd996a 100644 --- a/charts/manager/crds/greenhouse.sap_clusters.yaml +++ b/charts/manager/crds/greenhouse.sap_clusters.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: clusters.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_organizations.yaml b/charts/manager/crds/greenhouse.sap_organizations.yaml index 556281357..8ac21917b 100644 --- a/charts/manager/crds/greenhouse.sap_organizations.yaml +++ b/charts/manager/crds/greenhouse.sap_organizations.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: organizations.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_plugindefinitions.yaml b/charts/manager/crds/greenhouse.sap_plugindefinitions.yaml index d85bbd8d6..f61a82921 100644 --- a/charts/manager/crds/greenhouse.sap_plugindefinitions.yaml +++ b/charts/manager/crds/greenhouse.sap_plugindefinitions.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: plugindefinitions.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_pluginpresets.yaml b/charts/manager/crds/greenhouse.sap_pluginpresets.yaml index 08ac86452..55af83d7c 100644 --- a/charts/manager/crds/greenhouse.sap_pluginpresets.yaml +++ b/charts/manager/crds/greenhouse.sap_pluginpresets.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: pluginpresets.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_plugins.yaml b/charts/manager/crds/greenhouse.sap_plugins.yaml index 1ace7d008..e016b0476 100644 --- a/charts/manager/crds/greenhouse.sap_plugins.yaml +++ b/charts/manager/crds/greenhouse.sap_plugins.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: plugins.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml b/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml index 0e6e02d1c..ed3db6e8a 100644 --- a/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml +++ b/charts/manager/crds/greenhouse.sap_teamrolebindings.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: teamrolebindings.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_teamroles.yaml b/charts/manager/crds/greenhouse.sap_teamroles.yaml index ecd3cc24f..2439217df 100644 --- a/charts/manager/crds/greenhouse.sap_teamroles.yaml +++ b/charts/manager/crds/greenhouse.sap_teamroles.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: teamroles.greenhouse.sap spec: group: greenhouse.sap diff --git a/charts/manager/crds/greenhouse.sap_teams.yaml b/charts/manager/crds/greenhouse.sap_teams.yaml index 764010e5a..abe5ca3ca 100644 --- a/charts/manager/crds/greenhouse.sap_teams.yaml +++ b/charts/manager/crds/greenhouse.sap_teams.yaml @@ -6,7 +6,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.20.0 + controller-gen.kubebuilder.io/version: v0.20.1 name: teams.greenhouse.sap spec: group: greenhouse.sap From e7485d08c028a4a458e344a5c6d26885fcf1907b Mon Sep 17 00:00:00 2001 From: David Gogl <1381862+kengou@users.noreply.github.com> Date: Tue, 19 May 2026 11:47:57 +0200 Subject: [PATCH 11/12] fix review comment Signed-off-by: David Gogl <1381862+kengou@users.noreply.github.com> --- api/v1alpha1/organization_types.go | 7 ++- .../crds/greenhouse.sap_organizations.yaml | 7 ++- docs/reference/api/index.html | 7 ++- docs/reference/api/openapi.yaml | 7 ++- internal/controller/organization/dex.go | 8 +-- internal/dex/connector.go | 36 +++++++++---- internal/dex/connector_test.go | 53 +++++++++++++++++++ internal/test/resources.go | 3 +- 8 files changed, 105 insertions(+), 23 deletions(-) diff --git a/api/v1alpha1/organization_types.go b/api/v1alpha1/organization_types.go index 1955d1a67..ade7ec622 100755 --- a/api/v1alpha1/organization_types.go +++ b/api/v1alpha1/organization_types.go @@ -90,8 +90,11 @@ type OIDCConfig struct { } type OIDCExtraConfig struct { - // InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. - // This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations). + // InsecureSkipEmailVerified, if set to true, forces the email_verified claim to true + // regardless of the value reported by the upstream IdP, including overriding an explicit + // email_verified=false. Enable only for IdPs where the email address is verified by other + // means, e.g. Keycloak with SAML or user federation, or providers that omit the claim + // entirely such as some Okta, EntraID or CloudFoundry configurations. // +kubebuilder:default:=false InsecureSkipEmailVerified bool `json:"insecureSkipEmailVerified,omitempty"` // UserIDClaim is the claim to be used as both user ID and username. diff --git a/charts/manager/crds/greenhouse.sap_organizations.yaml b/charts/manager/crds/greenhouse.sap_organizations.yaml index fca425081..7e37eaaed 100644 --- a/charts/manager/crds/greenhouse.sap_organizations.yaml +++ b/charts/manager/crds/greenhouse.sap_organizations.yaml @@ -99,8 +99,11 @@ spec: insecureSkipEmailVerified: default: false description: |- - InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. - This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations). + InsecureSkipEmailVerified, if set to true, forces the email_verified claim to true + regardless of the value reported by the upstream IdP, including overriding an explicit + email_verified=false. Enable only for IdPs where the email address is verified by other + means, e.g. Keycloak with SAML or user federation, or providers that omit the claim + entirely such as some Okta, EntraID or CloudFoundry configurations. type: boolean userIDClaim: default: login_name diff --git a/docs/reference/api/index.html b/docs/reference/api/index.html index d3d181a36..3c680f9b0 100644 --- a/docs/reference/api/index.html +++ b/docs/reference/api/index.html @@ -2124,8 +2124,11 @@

OIDCExtraConfig

diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index 6418f3628..e5630c236 100755 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -725,8 +725,11 @@ components: insecureSkipEmailVerified: default: false description: |- - InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. - This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations). + InsecureSkipEmailVerified, if set to true, forces the email_verified claim to true + regardless of the value reported by the upstream IdP, including overriding an explicit + email_verified=false. Enable only for IdPs where the email address is verified by other + means, e.g. Keycloak with SAML or user federation, or providers that omit the claim + entirely such as some Okta, EntraID or CloudFoundry configurations. type: boolean userIDClaim: default: login_name diff --git a/internal/controller/organization/dex.go b/internal/controller/organization/dex.go index 8826d39bc..8b6d79c28 100755 --- a/internal/controller/organization/dex.go +++ b/internal/controller/organization/dex.go @@ -113,11 +113,11 @@ func (r *OrganizationReconciler) reconcileDexConnector(ctx context.Context, org if err != nil { return err } - var userNameKey = "login_name" + var userIDClaim = "login_name" var skipEmailVerified = false if org.Spec.Authentication.OIDCConfig.ExtraConfig != nil { if org.Spec.Authentication.OIDCConfig.ExtraConfig.UserIDClaim != "" { - userNameKey = org.Spec.Authentication.OIDCConfig.ExtraConfig.UserIDClaim + userIDClaim = org.Spec.Authentication.OIDCConfig.ExtraConfig.UserIDClaim } skipEmailVerified = org.Spec.Authentication.OIDCConfig.ExtraConfig.InsecureSkipEmailVerified } @@ -126,8 +126,8 @@ func (r *OrganizationReconciler) reconcileDexConnector(ctx context.Context, org ClientID: clientID, ClientSecret: clientSecret, RedirectURI: redirectURL, - UserNameKey: userNameKey, - UserIDKey: userNameKey, + UserNameKey: userIDClaim, + UserIDKey: userIDClaim, InsecureSkipEmailVerified: skipEmailVerified, InsecureSkipVerify: true, InsecureEnableGroups: true, diff --git a/internal/dex/connector.go b/internal/dex/connector.go index c74fd8080..dbe7e25c3 100644 --- a/internal/dex/connector.go +++ b/internal/dex/connector.go @@ -53,20 +53,22 @@ func (c *OIDCConfig) Open(id string, logger *slog.Logger) (connector.Connector, } return &oidcConnector{ - conn: conn, - logger: logger, - client: c.client, - id: id, - keepUpstreamGroups: c.KeepUpstreamGroups, + conn: conn, + logger: logger, + client: c.client, + id: id, + keepUpstreamGroups: c.KeepUpstreamGroups, + overrideEmailVerified: c.InsecureSkipEmailVerified, }, nil } type oidcConnector struct { - conn connector.Connector - logger *slog.Logger - client client.Client - id string - keepUpstreamGroups bool + conn connector.Connector + logger *slog.Logger + client client.Client + id string + keepUpstreamGroups bool + overrideEmailVerified bool } func (c *oidcConnector) LoginURL(s connector.Scopes, callbackURL, state string) (string, []byte, error) { //nolint:gocritic @@ -79,6 +81,15 @@ func (c *oidcConnector) HandleCallback(s connector.Scopes, connData []byte, r *h return identity, err } + // overrideEmailVerified forces the email_verified claim to true regardless of the value + // reported by the upstream IdP, including overriding an explicit email_verified=false. + // It is required for IdPs that report email_verified=false (or omit the claim) for users + // federated via SAML, where the email is verified by other means. Enabled per organization + // via OIDCConfig.ExtraConfig.InsecureSkipEmailVerified. + if c.overrideEmailVerified { + identity.EmailVerified = true + } + groups, groupsErr := c.getGroups(c.id, identity.Groups, r.Context()) if groupsErr != nil { c.logger.Info("failed getting group", "groupID", c.id, "error", groupsErr) @@ -108,6 +119,11 @@ func (c *oidcConnector) Refresh(ctx context.Context, s connector.Scopes, identit return identity, err } + // Keep email_verified consistent with HandleCallback when the token is refreshed. + if c.overrideEmailVerified { + identity.EmailVerified = true + } + groups, groupsErr := c.getGroups(c.id, identity.Groups, ctx) if groupsErr != nil { c.logger.Info("failed getting groups", "connectorID", c.id, "error", groupsErr) diff --git a/internal/dex/connector_test.go b/internal/dex/connector_test.go index c8da16883..ad196cfda 100644 --- a/internal/dex/connector_test.go +++ b/internal/dex/connector_test.go @@ -6,6 +6,7 @@ package dex import ( "context" "log/slog" + "net/http" "os" "testing" @@ -142,3 +143,55 @@ var _ = Describe("Getting groups for token", func() { Expect(groups).To(Equal([]string{"organization:" + test.TestNamespace, "team:test-team-1", "test-team-category-1:test-team-1", "role:" + test.TestNamespace + ":admin", "team:test-team-2", "test-team-category-2:test-team-2", "team:test-team-3", "team:test-team-4"}), "The groups should be correct") }) }) + +// fakeUpstreamConnector is a minimal connector.Connector used to drive +// oidcConnector.HandleCallback and Refresh without a real upstream IdP. +type fakeUpstreamConnector struct { + identity connector.Identity +} + +var ( + _ connector.CallbackConnector = (*fakeUpstreamConnector)(nil) + _ connector.RefreshConnector = (*fakeUpstreamConnector)(nil) +) + +func (f *fakeUpstreamConnector) LoginURL(connector.Scopes, string, string) (loginURL string, connData []byte, err error) { + return "", nil, nil +} + +func (f *fakeUpstreamConnector) HandleCallback(connector.Scopes, []byte, *http.Request) (connector.Identity, error) { + return f.identity, nil +} + +func (f *fakeUpstreamConnector) Refresh(context.Context, connector.Scopes, connector.Identity) (connector.Identity, error) { + return f.identity, nil +} + +var _ = Describe("Overriding email_verified", func() { + It("Should force EmailVerified to true on HandleCallback when overrideEmailVerified is set", func() { + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + upstream := &fakeUpstreamConnector{identity: connector.Identity{Email: "user@example.com", EmailVerified: false}} + c := oidcConnector{conn: upstream, logger: logger, client: test.K8sClient, id: test.TestNamespace, overrideEmailVerified: true} + identity, err := c.HandleCallback(connector.Scopes{}, nil, &http.Request{}) + Expect(err).ToNot(HaveOccurred(), "There should be no error handling the callback") + Expect(identity.EmailVerified).To(BeTrue(), "EmailVerified should be forced to true even though the upstream reported false") + }) + + It("Should keep the upstream EmailVerified on HandleCallback when overrideEmailVerified is not set", func() { + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + upstream := &fakeUpstreamConnector{identity: connector.Identity{Email: "user@example.com", EmailVerified: false}} + c := oidcConnector{conn: upstream, logger: logger, client: test.K8sClient, id: test.TestNamespace, overrideEmailVerified: false} + identity, err := c.HandleCallback(connector.Scopes{}, nil, &http.Request{}) + Expect(err).ToNot(HaveOccurred(), "There should be no error handling the callback") + Expect(identity.EmailVerified).To(BeFalse(), "EmailVerified should reflect the upstream value") + }) + + It("Should force EmailVerified to true on Refresh when overrideEmailVerified is set", func() { + logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + upstream := &fakeUpstreamConnector{identity: connector.Identity{Email: "user@example.com", EmailVerified: false}} + c := oidcConnector{conn: upstream, logger: logger, client: test.K8sClient, id: test.TestNamespace, overrideEmailVerified: true} + identity, err := c.Refresh(context.TODO(), connector.Scopes{}, connector.Identity{}) + Expect(err).ToNot(HaveOccurred(), "There should be no error refreshing the identity") + Expect(identity.EmailVerified).To(BeTrue(), "EmailVerified should be forced to true even though the upstream reported false") + }) +}) diff --git a/internal/test/resources.go b/internal/test/resources.go index 9aaa41660..8a2fe8a91 100644 --- a/internal/test/resources.go +++ b/internal/test/resources.go @@ -134,7 +134,8 @@ func WithOIDCConfig(issuer, secretName, clientIDKey, clientSecretKey string) fun } // WithOIDCExtraConfig sets the OIDCExtraConfig on an Organization's OIDCConfig. -// Must be used after WithOIDCConfig to ensure OIDCConfig is initialized. +// Must be used after WithOIDCConfig: WithOIDCConfig replaces OIDCConfig wholesale, +// so any ExtraConfig set beforehand would be discarded. func WithOIDCExtraConfig(userIDClaim string, insecureSkipEmailVerified bool) func(*greenhousev1alpha1.Organization) { return func(org *greenhousev1alpha1.Organization) { if org.Spec.Authentication == nil { From 4801d2594efcc5c62ab2353e9c90d7f2ccab4fa6 Mon Sep 17 00:00:00 2001 From: David Gogl <1381862+kengou@users.noreply.github.com> Date: Tue, 19 May 2026 11:56:42 +0200 Subject: [PATCH 12/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- internal/dex/connector.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/dex/connector.go b/internal/dex/connector.go index dbe7e25c3..19c0fab92 100644 --- a/internal/dex/connector.go +++ b/internal/dex/connector.go @@ -85,7 +85,7 @@ func (c *oidcConnector) HandleCallback(s connector.Scopes, connData []byte, r *h // reported by the upstream IdP, including overriding an explicit email_verified=false. // It is required for IdPs that report email_verified=false (or omit the claim) for users // federated via SAML, where the email is verified by other means. Enabled per organization - // via OIDCConfig.ExtraConfig.InsecureSkipEmailVerified. + // via the effective InsecureSkipEmailVerified flag. if c.overrideEmailVerified { identity.EmailVerified = true }
FieldDescription
insecureSkipEmailVerified
bool diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index fa5585217..d3a5e6f14 100755 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -669,13 +669,21 @@ components: - key - name type: object - insecureSkipEmailVerified: - default: false - description: InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. - enum: - - true - - false - type: boolean + extraConfig: + description: ExtraClaims contains additional configuration for extra claims. + properties: + insecureSkipEmailVerified: + default: false + description: InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. + enum: + - true + - false + type: boolean + userIDClaim: + default: login_name + description: UserIDClaim is the claim to be used as user ID. + type: string + type: object issuer: description: Issuer is the URL of the identity service. type: string @@ -691,10 +699,6 @@ components: RedirectURI is the redirect URI to be used for the OIDC flow against the upstream IdP. If none is specified, the Greenhouse ID proxy will be used. type: string - userIDClaim: - default: login_name - description: UserIDClaim is the claim to be used as user ID. - type: string required: - clientIDReference - clientSecretReference From 2290e271d4d5ef443cfff7139e294fd130b1a33d Mon Sep 17 00:00:00 2001 From: "cloud-operator-bot[bot]" <224791424+cloud-operator-bot[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:30:48 +0000 Subject: [PATCH 07/12] Automatic generation of CRD API Docs --- api/v1alpha1/zz_generated.deepcopy.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 8f9ccea31..0e5eb1d97 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -858,6 +858,21 @@ func (in *OIDCConfig) DeepCopy() *OIDCConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCExtraConfig) DeepCopyInto(out *OIDCExtraConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCExtraConfig. +func (in *OIDCExtraConfig) DeepCopy() *OIDCExtraConfig { + if in == nil { + return nil + } + out := new(OIDCExtraConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OptionsOverride) DeepCopyInto(out *OptionsOverride) { *out = *in From 52ce0dfddd9947912f2ee80e6135331ed0baf38b Mon Sep 17 00:00:00 2001 From: David Gogl <1381862+kengou@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:33:47 +0100 Subject: [PATCH 08/12] Update config/samples/organization/demo.yaml Co-authored-by: Abhijith Ravindra <137736216+abhijith-darshan@users.noreply.github.com> --- config/samples/organization/demo.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/samples/organization/demo.yaml b/config/samples/organization/demo.yaml index cac535baf..8bcf7650b 100644 --- a/config/samples/organization/demo.yaml +++ b/config/samples/organization/demo.yaml @@ -53,5 +53,6 @@ spec: name: demo-oidc issuer: https://global.accounts.dev redirectURI: https://bogus.accounts.foo - insecureSkipEmailVerified: true - userIDClaim: email + extraConfig: + insecureSkipEmailVerified: true + userIDClaim: email From 9480ff50606db467db15baa404bd19ab1ff8fda8 Mon Sep 17 00:00:00 2001 From: David Gogl <1381862+kengou@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:23:28 +0100 Subject: [PATCH 09/12] fix issues, add test and run generate Signed-off-by: David Gogl <1381862+kengou@users.noreply.github.com> --- api/v1alpha1/organization_types.go | 9 +-- .../crds/greenhouse.sap_organizations.yaml | 18 +++--- config/samples/organization/demo.yaml | 1 - docs/reference/api/index.html | 8 ++- docs/reference/api/openapi.yaml | 13 +++-- internal/controller/organization/dex.go | 6 +- .../organization_controller_test.go | 57 +++++++++++++++++++ internal/test/resources.go | 17 ++++++ types/typescript/schema.d.ts | 43 +++++++------- 9 files changed, 124 insertions(+), 48 deletions(-) diff --git a/api/v1alpha1/organization_types.go b/api/v1alpha1/organization_types.go index 7120582f0..e55d4abdf 100755 --- a/api/v1alpha1/organization_types.go +++ b/api/v1alpha1/organization_types.go @@ -85,16 +85,17 @@ type OIDCConfig struct { // OAuth2ClientRedirectURIs are a registered set of redirect URIs. When redirecting from the idproxy to // the client application, the URI requested to redirect to must be contained in this list. OAuth2ClientRedirectURIs []string `json:"oauth2ClientRedirectURIs,omitempty"` - // ExtraClaims contains additional configuration for extra claims. + // ExtraConfig contains additional OIDC configuration for claim mapping and token validation behavior. ExtraConfig *OIDCExtraConfig `json:"extraConfig,omitempty"` } type OIDCExtraConfig struct { - // InsecureSkipEmailVerified allows to skip the verification of the "email_verified" claim in ID tokens. + // InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. + // This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations). // +kubebuilder:default:=false - // +kubebuilder:validation:Enum:=true;false InsecureSkipEmailVerified bool `json:"insecureSkipEmailVerified,omitempty"` - // UserIDClaim is the claim to be used as user ID. + // UserIDClaim is the claim to be used as both user ID and username. + // When set, it overrides both UserIDKey and UserNameKey in the dex OIDC connector config. // +kubebuilder:default:="login_name" UserIDClaim string `json:"userIDClaim,omitempty"` } diff --git a/charts/manager/crds/greenhouse.sap_organizations.yaml b/charts/manager/crds/greenhouse.sap_organizations.yaml index 210aedff7..556281357 100644 --- a/charts/manager/crds/greenhouse.sap_organizations.yaml +++ b/charts/manager/crds/greenhouse.sap_organizations.yaml @@ -93,22 +93,20 @@ spec: - name type: object extraConfig: - description: ExtraClaims contains additional configuration - for extra claims. + description: ExtraConfig contains additional OIDC configuration + for claim mapping and token validation behavior. properties: insecureSkipEmailVerified: default: false - description: InsecureSkipEmailVerified allows to skip - the verification of the "email_verified" claim in ID - tokens. - enum: - - true - - false + description: |- + InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. + This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations). type: boolean userIDClaim: default: login_name - description: UserIDClaim is the claim to be used as user - ID. + description: |- + UserIDClaim is the claim to be used as both user ID and username. + When set, it overrides both UserIDKey and UserNameKey in the dex OIDC connector config. type: string type: object issuer: diff --git a/config/samples/organization/demo.yaml b/config/samples/organization/demo.yaml index 8bcf7650b..d5a55c29c 100644 --- a/config/samples/organization/demo.yaml +++ b/config/samples/organization/demo.yaml @@ -54,5 +54,4 @@ spec: issuer: https://global.accounts.dev redirectURI: https://bogus.accounts.foo extraConfig: - insecureSkipEmailVerified: true userIDClaim: email diff --git a/docs/reference/api/index.html b/docs/reference/api/index.html index 4d31db72c..b4f9915db 100644 --- a/docs/reference/api/index.html +++ b/docs/reference/api/index.html @@ -2001,7 +2001,7 @@

OIDCConfig

-

ExtraClaims contains additional configuration for extra claims.

+

ExtraConfig contains additional OIDC configuration for claim mapping and token validation behavior.

-

InsecureSkipEmailVerified allows to skip the verification of the “email_verified” claim in ID tokens.

+

InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. +This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations).

-

UserIDClaim is the claim to be used as user ID.

+

UserIDClaim is the claim to be used as both user ID and username. +When set, it overrides both UserIDKey and UserNameKey in the dex OIDC connector config.

-

InsecureSkipEmailVerified if set to true, treats email_verified as true when the claim is absent from the ID token. -This does not override an explicit email_verified=false. Only enable for providers that omit the claim entirely (e.g. some Okta, EntraID or CloudFoundry configurations).

+

InsecureSkipEmailVerified, if set to true, forces the email_verified claim to true +regardless of the value reported by the upstream IdP, including overriding an explicit +email_verified=false. Enable only for IdPs where the email address is verified by other +means, e.g. Keycloak with SAML or user federation, or providers that omit the claim +entirely such as some Okta, EntraID or CloudFoundry configurations.