Skip to content

Commit 18563a7

Browse files
authored
feat: automatic detection and configuration of OpenShift's external OIDC authentication (#2127)
* chore: automatic detection and configuration of OpenShift's external OIDC authentication
1 parent e5d401f commit 18563a7

32 files changed

Lines changed: 913 additions & 214 deletions

api/v2/checluster_types.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -615,11 +615,11 @@ type Auth struct {
615615
// For OpenShift with built-in OAuth, this is the name of the `OAuthClient` resource used to set up identity federation.
616616
// +optional
617617
OAuthClientName string `json:"oAuthClientName,omitempty"`
618-
// Defines the OAuth client secret.
619-
// It can either be a plain text secret value or the name of a Kubernetes secret
620-
// containing a key `oAuthSecret` with the secret value. The Kubernetes secret must exist in the same namespace
621-
// as the `CheCluster` resource and have the label `app.kubernetes.io/part-of=che.eclipse.org`.
622-
// For OpenShift with built-in OAuth, this is the secret set in the `OAuthClient` resource used to set up identity federation.
618+
// For OIDC, this is the client secret issued by the Identity Provider for the configured OIDC client.
619+
// For OpenShift with built-in OAuth, this is the secret configured in the OAuthClient for OpenShift OAuth integration.
620+
// The value can either be a plain text secret value (deprecated) or the name of a Kubernetes secret
621+
// that contains the secret value under the `oAuthSecret` key. The Kubernetes secret must exist in the same namespace
622+
// as the `CheCluster` resource namespace and must have a `app.kubernetes.io/part-of=che.eclipse.org` label.
623623
// +optional
624624
OAuthSecret string `json:"oAuthSecret,omitempty"`
625625
// Defines the scope requested from the OIDC provider.

bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ metadata:
8686
categories: Developer Tools
8787
certified: "false"
8888
containerImage: quay.io/eclipse/che-operator:next
89-
createdAt: "2026-05-21T07:42:52Z"
89+
createdAt: "2026-05-26T12:11:24Z"
9090
description: A Kube-native development solution that delivers portable and collaborative
9191
developer workspaces.
9292
features.operators.openshift.io/cnf: "false"
@@ -108,7 +108,7 @@ metadata:
108108
operatorframework.io/arch.amd64: supported
109109
operatorframework.io/arch.arm64: supported
110110
operatorframework.io/os.linux: supported
111-
name: eclipse-che.v7.118.0-986.next
111+
name: eclipse-che.v7.119.0-989.next
112112
namespace: placeholder
113113
spec:
114114
apiservicedefinitions: {}
@@ -772,6 +772,7 @@ spec:
772772
- cluster
773773
resources:
774774
- proxies
775+
- authentications
775776
verbs:
776777
- get
777778
- apiGroups:
@@ -1152,7 +1153,7 @@ spec:
11521153
name: gateway-authorization-sidecar
11531154
- image: quay.io/che-incubator/header-rewrite-proxy:latest
11541155
name: gateway-header-sidecar
1155-
version: 7.118.0-986.next
1156+
version: 7.119.0-989.next
11561157
webhookdefinitions:
11571158
- admissionReviewVersions:
11581159
- v1

bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22749,11 +22749,11 @@ spec:
2274922749
type: string
2275022750
oAuthSecret:
2275122751
description: |-
22752-
Defines the OAuth client secret.
22753-
It can either be a plain text secret value or the name of a Kubernetes secret
22754-
containing a key `oAuthSecret` with the secret value. The Kubernetes secret must exist in the same namespace
22755-
as the `CheCluster` resource and have the label `app.kubernetes.io/part-of=che.eclipse.org`.
22756-
For OpenShift with built-in OAuth, this is the secret set in the `OAuthClient` resource used to set up identity federation.
22752+
For OIDC, this is the client secret issued by the Identity Provider for the configured OIDC client.
22753+
For OpenShift with built-in OAuth, this is the secret configured in the OAuthClient for OpenShift OAuth integration.
22754+
The value can either be a plain text secret value (deprecated) or the name of a Kubernetes secret
22755+
that contains the secret value under the `oAuthSecret` key. The Kubernetes secret must exist in the same namespace
22756+
as the `CheCluster` resource namespace and must have a `app.kubernetes.io/part-of=che.eclipse.org` label.
2275722757
type: string
2275822758
type: object
2275922759
domain:

config/crd/bases/org.eclipse.che_checlusters.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22650,11 +22650,11 @@ spec:
2265022650
type: string
2265122651
oAuthSecret:
2265222652
description: |-
22653-
Defines the OAuth client secret.
22654-
It can either be a plain text secret value or the name of a Kubernetes secret
22655-
containing a key `oAuthSecret` with the secret value. The Kubernetes secret must exist in the same namespace
22656-
as the `CheCluster` resource and have the label `app.kubernetes.io/part-of=che.eclipse.org`.
22657-
For OpenShift with built-in OAuth, this is the secret set in the `OAuthClient` resource used to set up identity federation.
22653+
For OIDC, this is the client secret issued by the Identity Provider for the configured OIDC client.
22654+
For OpenShift with built-in OAuth, this is the secret configured in the OAuthClient for OpenShift OAuth integration.
22655+
The value can either be a plain text secret value (deprecated) or the name of a Kubernetes secret
22656+
that contains the secret value under the `oAuthSecret` key. The Kubernetes secret must exist in the same namespace
22657+
as the `CheCluster` resource namespace and must have a `app.kubernetes.io/part-of=che.eclipse.org` label.
2265822658
type: string
2265922659
type: object
2266022660
domain:

config/rbac/cluster_role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ rules:
247247
- config.openshift.io
248248
resources:
249249
- proxies
250+
- authentications
250251
resourceNames:
251252
- cluster
252253
verbs:

controllers/che/authentication.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
//
2+
// Copyright (c) 2019-2026 Red Hat, Inc.
3+
// This program and the accompanying materials are made
4+
// available under the terms of the Eclipse Public License 2.0
5+
// which is available at https://www.eclipse.org/legal/epl-2.0/
6+
//
7+
// SPDX-License-Identifier: EPL-2.0
8+
//
9+
// Contributors:
10+
// Red Hat, Inc. - initial API and implementation
11+
//
12+
13+
package che
14+
15+
import (
16+
"context"
17+
"fmt"
18+
"slices"
19+
20+
"github.com/eclipse-che/che-operator/pkg/common/chetypes"
21+
"github.com/eclipse-che/che-operator/pkg/common/infrastructure"
22+
configv1 "github.com/openshift/api/config/v1"
23+
corev1 "k8s.io/api/core/v1"
24+
"k8s.io/apimachinery/pkg/types"
25+
)
26+
27+
const (
28+
// OpenShift external OIDC authentication constants.
29+
// See: https://docs.redhat.com/en/documentation/openshift_container_platform/4.20/html/authentication_and_authorization/external-auth
30+
openshiftConfigNamespace = "openshift-config"
31+
issuerCAKey = "ca-bundle.crt"
32+
oidcClientSecretKey = "clientSecret"
33+
)
34+
35+
// ResolveAuthentication builds an Authentication config from the CheCluster spec,
36+
// falling back to the OpenShift cluster Authentication resource for any unset fields.
37+
func ResolveAuthentication(ctx *chetypes.DeployContext) (*chetypes.Authentication, error) {
38+
authentication := &chetypes.Authentication{
39+
UsernameClaim: ctx.CheCluster.Spec.Components.CheServer.ExtraProperties["CHE_OIDC_USERNAME__CLAIM"],
40+
UsernamePrefix: ctx.CheCluster.Spec.Components.CheServer.ExtraProperties["CHE_OIDC_USERNAME__PREFIX"],
41+
GroupsClaim: ctx.CheCluster.Spec.Components.CheServer.ExtraProperties["CHE_OIDC_GROUPS__CLAIM"],
42+
GroupsPrefix: ctx.CheCluster.Spec.Components.CheServer.ExtraProperties["CHE_OIDC_GROUPS__PREFIX"],
43+
IssuerURL: ctx.CheCluster.Spec.Networking.Auth.IdentityProviderURL,
44+
// OIDC client ID must be explicitly defined; the openshift-console client
45+
// cannot be reused because it has a different callback URL.
46+
ClientId: ctx.CheCluster.Spec.Networking.Auth.OAuthClientName,
47+
}
48+
49+
// must be outside main `if` condition
50+
if ctx.CheCluster.Spec.Networking.Auth.OAuthSecret != "" {
51+
// `OAuthSecret` can be a Kubernetes Secret name in CheCluster namespace
52+
// or a literal value; resolve accordingly.
53+
clientSecret, err := resolveOAuthSecretInCheNamespace(ctx)
54+
if err != nil {
55+
return nil, fmt.Errorf("failed to resolve secret: %w", err)
56+
}
57+
authentication.ClientSecret = clientSecret
58+
}
59+
60+
if infrastructure.IsOpenShiftExternalAuth() {
61+
clusterAuthentication := &configv1.Authentication{}
62+
err := ctx.ClusterAPI.NonCachingClient.Get(context.TODO(), types.NamespacedName{Name: "cluster"}, clusterAuthentication)
63+
if err != nil {
64+
return nil, fmt.Errorf("failed to fetch authentication config: %w", err)
65+
}
66+
67+
if clusterAuthentication.Spec.Type != configv1.AuthenticationTypeOIDC {
68+
return nil, fmt.Errorf("authentication type is not OIDC")
69+
}
70+
71+
if len(clusterAuthentication.Spec.OIDCProviders) == 0 {
72+
return nil, fmt.Errorf("no OIDC providers configured")
73+
}
74+
75+
if len(clusterAuthentication.Spec.OIDCProviders) != 1 {
76+
return nil, fmt.Errorf("multiple OIDC providers configured, expected exactly one")
77+
}
78+
79+
oidcProvider := clusterAuthentication.Spec.OIDCProviders[0]
80+
81+
// issuer URL
82+
if authentication.IssuerURL == "" {
83+
authentication.IssuerURL = oidcProvider.Issuer.URL
84+
}
85+
86+
// issuer certificate authority
87+
if authentication.IssuerURL == oidcProvider.Issuer.URL {
88+
if oidcProvider.Issuer.CertificateAuthority.Name != "" {
89+
issuerCA, err := readIssuerCA(oidcProvider.Issuer.CertificateAuthority.Name, ctx)
90+
if err != nil {
91+
return nil, fmt.Errorf("failed to read issuer CA: %w", err)
92+
}
93+
authentication.IssuerCA = issuerCA
94+
}
95+
}
96+
97+
// username/groups claim mappings
98+
if authentication.GroupsClaim == "" {
99+
authentication.GroupsClaim = oidcProvider.ClaimMappings.Groups.Claim
100+
}
101+
if authentication.GroupsClaim != "" && authentication.GroupsPrefix == "" {
102+
authentication.GroupsPrefix = oidcProvider.ClaimMappings.Groups.Prefix
103+
}
104+
105+
if authentication.UsernameClaim == "" {
106+
authentication.UsernameClaim = oidcProvider.ClaimMappings.Username.Claim
107+
}
108+
if authentication.UsernameClaim != "" && authentication.UsernamePrefix == "" {
109+
switch oidcProvider.ClaimMappings.Username.PrefixPolicy {
110+
case configv1.NoOpinion:
111+
// See `NoOpinion` description
112+
if authentication.UsernameClaim != "email" {
113+
authentication.UsernamePrefix = fmt.Sprintf("%s#", authentication.IssuerURL)
114+
}
115+
case configv1.Prefix:
116+
if oidcProvider.ClaimMappings.Username.Prefix != nil {
117+
authentication.UsernamePrefix = oidcProvider.ClaimMappings.Username.Prefix.PrefixString
118+
}
119+
}
120+
}
121+
122+
// client secret
123+
if len(authentication.ClientSecret) == 0 {
124+
idx := slices.IndexFunc(oidcProvider.OIDCClients, func(config configv1.OIDCClientConfig) bool {
125+
return config.ClientID == authentication.ClientId
126+
})
127+
128+
if idx != -1 {
129+
oidcClient := oidcProvider.OIDCClients[idx]
130+
if oidcClient.ClientSecret.Name != "" {
131+
clientSecret, err := resolveClientSecretInOpenShiftConfigNamespace(oidcClient.ClientSecret.Name, ctx)
132+
if err != nil {
133+
return nil, fmt.Errorf("failed to read client secret: %w", err)
134+
}
135+
authentication.ClientSecret = clientSecret
136+
}
137+
}
138+
}
139+
}
140+
141+
return authentication, nil
142+
}
143+
144+
func resolveClientSecretInOpenShiftConfigNamespace(secretName string, ctx *chetypes.DeployContext) ([]byte, error) {
145+
secret := &corev1.Secret{}
146+
err := ctx.ClusterAPI.NonCachingClient.Get(
147+
context.TODO(),
148+
types.NamespacedName{Name: secretName, Namespace: openshiftConfigNamespace},
149+
secret,
150+
)
151+
if err != nil {
152+
return nil, err
153+
}
154+
155+
value, ok := secret.Data[oidcClientSecretKey]
156+
if ok {
157+
return value, nil
158+
}
159+
160+
return nil, fmt.Errorf("client secret not found in: %s", secretName)
161+
}
162+
163+
func resolveOAuthSecretInCheNamespace(ctx *chetypes.DeployContext) ([]byte, error) {
164+
secret := &corev1.Secret{}
165+
exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound(
166+
context.TODO(),
167+
types.NamespacedName{
168+
Name: ctx.CheCluster.Spec.Networking.Auth.OAuthSecret,
169+
Namespace: ctx.CheCluster.Namespace,
170+
},
171+
secret,
172+
)
173+
if err != nil {
174+
return nil, err
175+
}
176+
if exists {
177+
value, ok := secret.Data["oAuthSecret"]
178+
if ok {
179+
return value, nil
180+
}
181+
182+
return nil, fmt.Errorf("client secret not found in: %s", ctx.CheCluster.Spec.Networking.Auth.OAuthSecret)
183+
}
184+
185+
// Backward compatibility: treat as a literal secret value, not a reference.
186+
return []byte(ctx.CheCluster.Spec.Networking.Auth.OAuthSecret), nil
187+
}
188+
189+
func readIssuerCA(cmName string, ctx *chetypes.DeployContext) (string, error) {
190+
cm := &corev1.ConfigMap{}
191+
err := ctx.ClusterAPI.NonCachingClient.Get(
192+
context.TODO(),
193+
types.NamespacedName{Name: cmName, Namespace: openshiftConfigNamespace},
194+
cm,
195+
)
196+
if err != nil {
197+
return "", err
198+
}
199+
200+
ca, ok := cm.Data[issuerCAKey]
201+
if !ok {
202+
return "", fmt.Errorf("issuer CA not found in the ConfigMap %s", cmName)
203+
}
204+
205+
return ca, nil
206+
}

0 commit comments

Comments
 (0)