]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Improve oauth2 sso configuration 67417/head
authorAlexandre JARDON <28548335+webalexeu@users.noreply.github.com>
Fri, 24 Apr 2026 09:05:22 +0000 (11:05 +0200)
committerAlexandre JARDON <28548335+webalexeu@users.noreply.github.com>
Fri, 24 Apr 2026 14:16:09 +0000 (16:16 +0200)
Add possibility to configure email_domains and scope parameters in oauth2 sso configuration
Fixes: https://tracker.ceph.com/issues/75020
mgr/dashboard: Fix oauth2 sso missing jti claim review
Fixes: https://tracker.ceph.com/issues/75022
mgr/dashboard: Improve oauth2 sso configuration
Add possibility to configure ssl_insecure_skip_verify parameter in oauth2 sso configuration
Fixes: https://tracker.ceph.com/issues/75984
Signed-off-by: Alexandre Jardon <alexandre.jardon@webalex.eu>
doc/cephadm/services/oauth2-proxy.rst
src/pybind/mgr/cephadm/services/oauth2_proxy.py
src/pybind/mgr/cephadm/templates/services/oauth2-proxy/oauth2-proxy.conf.j2
src/pybind/mgr/cephadm/tests/services/test_mgmt_gateway.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts
src/pybind/mgr/dashboard/services/auth/auth.py
src/python-common/ceph/deployment/service_spec.py

index 515ba4410de6607f5ca7f81f7da979ebfbaed76d..4f0ab2e84172c021d3e1f8c67afd6860ebbbac51 100644 (file)
@@ -118,6 +118,7 @@ A non-exhaustive list of important limitations for the `oauth2-proxy` service fo
 
 * High-availability configurations for `oauth2-proxy` itself are not supported.
 * Proper configuration of the IDP and OAuth2 parameters is crucial to avoid authentication failures. Misconfigurations can lead to access issues.
+* IDP must include the jti claim in the issued JWT token because the Ceph Dashboard relies on this value to verify the token's validity.
 
 
 Container images
index 5a36b5a6adb347008df81c6d08b64c973dd0cd16..66d53bbe9dd51ef96e079ea719d481247c906252 100644 (file)
@@ -64,10 +64,12 @@ class OAuth2ProxyService(CephadmService):
         svc_spec = cast(OAuth2ProxySpec, self.mgr.spec_store[daemon_spec.service_name].spec)
         allowlist_domains = copy(svc_spec.allowlist_domains) or []
         allowlist_domains += self.get_service_ips_and_hosts('mgmt-gateway')
+        email_domains = copy(svc_spec.email_domains) or []
         context = {
             'spec': svc_spec,
             'cookie_secret': svc_spec.cookie_secret,
             'allowlist_domains': allowlist_domains,
+            'email_domains': email_domains,
             'redirect_url': svc_spec.redirect_url or self.get_redirect_url()
         }
 
index c8d9f920adf5ae16c2488889255c2388c102da87..5244f1e72c5bb7c5f2a380ebc72f1d8382261569 100644 (file)
@@ -14,8 +14,13 @@ oidc_issuer_url= "{{ spec.oidc_issuer_url }}"
 {% if redirect_url %}
 redirect_url= "{{ redirect_url }}"
 {% endif %}
+{% if spec.scope %}
+scope= "{{ spec.scope }}"
+{% endif %}
 
+{% if spec.ssl_insecure_skip_verify %}
 ssl_insecure_skip_verify=true
+{% endif %}
 
 # following configuration is needed to avoid getting Forbidden
 # when using chrome like browsers as they handle 3rd party cookies
@@ -33,5 +38,5 @@ set_xauthrequest= true
 
 # Secret value for encrypting cookies.
 cookie_secret= "{{ cookie_secret }}"
-email_domains= "*"
+email_domains= "{{ (email_domains | join(',')) or '*' }}"
 whitelist_domains= "{{ allowlist_domains | join(',') }}"
index 6e79311e88dd0d642c53aa1ae8ac9e6b80cfbc82..63fdef636d6794915f9bd343593c0775f9f31227 100644 (file)
@@ -779,7 +779,6 @@ class TestMgmtGateway:
                                          oidc_issuer_url= "http://192.168.10.10:8888/dex"
                                          redirect_url= "{redirect_url}"
 
-                                         ssl_insecure_skip_verify=true
 
                                          # following configuration is needed to avoid getting Forbidden
                                          # when using chrome like browsers as they handle 3rd party cookies
index 648b0282202d39737a63d88c0dd34755904096d7..9be973353be5c6593cd585ca7f55d3fb57974ede 100644 (file)
           }
         </ng-template>
       </div>
+      <!-- Scope -->
+      <div class="form-item">
+        <cd-text-label-list formControlName="scope"
+                            label="Scope"
+                            helperText="OAuth scope specification."
+                            placeholder="openid profile email">
+        </cd-text-label-list>
+      </div>
+      <!-- Email_domains -->
+      <div class="form-item">
+        <cd-text-label-list formControlName="email_domains"
+                            label="Email domains"
+                            helperText="Email domains to be allowed."
+                            placeholder="*">
+        </cd-text-label-list>
+      </div>
       <!-- Allowlist_domains -->
       <div class="form-item">
         <cd-text-label-list formControlName="allowlist_domains"
                             placeholder="192.168.100.1:8080">
         </cd-text-label-list>
       </div>
+      <!-- SSL Insecure Skip Verify -->
+      <div class="form-item">
+        <cds-checkbox i18n-label
+                      formControlName="ssl_insecure_skip_verify">
+          Skip TLS verification
+          <cd-help-text i18n>
+            Skip TLS certificate verification for the OIDC provider. Use only in non-production environments.
+          </cd-help-text>
+        </cds-checkbox>
+      </div>
       }
 
       @if (!serviceForm.controls.unmanaged.value && ['mgmt-gateway'].includes(serviceForm.controls.service_type.value))
index 6ecb3ba29158b58515db427de13e09358297986d..cd17d88fb25c949f53bebeef6a815e3b0d88dbbd 100644 (file)
@@ -657,7 +657,10 @@ export class ServiceFormComponent extends CdForm implements OnInit {
       ],
       https_address: [null, [CdValidators.oauthAddressTest()]],
       redirect_url: [null],
-      allowlist_domains: [null]
+      scope: [null],
+      email_domains: [null],
+      allowlist_domains: [null],
+      ssl_insecure_skip_verify: [false]
     });
   }
 
@@ -915,7 +918,10 @@ export class ServiceFormComponent extends CdForm implements OnInit {
                 'client_secret',
                 'oidc_issuer_url',
                 'redirect_url',
-                'allowlist_domains'
+                'scope',
+                'email_domains',
+                'allowlist_domains',
+                'ssl_insecure_skip_verify'
               ];
               oauth2SpecKeys.forEach((key) => {
                 this.serviceForm.get(key).setValue(response[0].spec[key]);
@@ -1433,6 +1439,12 @@ export class ServiceFormComponent extends CdForm implements OnInit {
           serviceSpec['oidc_issuer_url'] = values['oidc_issuer_url']?.trim();
           serviceSpec['https_address'] = values['https_address']?.trim();
           serviceSpec['redirect_url'] = values['redirect_url']?.trim();
+          serviceSpec['scope'] = values['scope']?.join(' ');
+          if (values['email_domains']) {
+            serviceSpec['email_domains'] = values['email_domains']?.map((emailDomain: string) => {
+              return emailDomain.trim();
+            });
+          }
           if (values['allowlist_domains']) {
             serviceSpec['allowlist_domains'] = values['allowlist_domains']?.map(
               (allowlistDomain: string) => {
@@ -1440,6 +1452,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
               }
             );
           }
+          serviceSpec['ssl_insecure_skip_verify'] = values['ssl_insecure_skip_verify'];
           if (values['ssl']) {
             this.applySslCertificateConfig(serviceSpec, values);
           }
index d49a9528faa3f2bdb060ed581b20dbd964368d2a..eaa0440673b563fcbf7b0856c60491cbce1912fe 100644 (file)
@@ -227,16 +227,19 @@ class JwtManager(object):
     def get_user(cls, token):
         try:
             dtoken = cls.decode_token(token)
-            if 'jti' in dtoken and not cls.is_blocklisted(dtoken['jti']):
-                user = AuthManager.get_user(dtoken['username'])
-                if 'iat' in dtoken and user.last_update <= dtoken['iat']:
-                    return user
-                cls.logger.debug(  # type: ignore
-                    "user info changed after token was issued, iat=%s last_update=%s",
-                    dtoken['iat'], user.last_update
-                )
+            if 'jti' in dtoken:
+                if not cls.is_blocklisted(dtoken['jti']):
+                    user = AuthManager.get_user(dtoken['username'])
+                    if 'iat' in dtoken and user.last_update <= dtoken['iat']:
+                        return user
+                    cls.logger.debug(  # type: ignore
+                        "user info changed after token was issued, iat=%s last_update=%s",
+                        dtoken['iat'], user.last_update
+                    )
+                else:
+                    cls.logger.debug('Token is block-listed')  # type: ignore
             else:
-                cls.logger.debug('Token is block-listed')  # type: ignore
+                cls.logger.debug('Missing jti claim in token')  # type: ignore
         except ExpiredSignatureError:
             cls.logger.debug("Token has expired")  # type: ignore
         except InvalidTokenError:
index 246a9b898c6b8d87da3b1537ba2ac30e9608cda9..c0df3ace01ba9a00aace35d28326d19525ce09a3 100644 (file)
@@ -2563,13 +2563,16 @@ class OAuth2ProxySpec(ServiceSpec):
                  client_secret: Optional[str] = None,
                  oidc_issuer_url: Optional[str] = None,
                  redirect_url: Optional[str] = None,
+                 scope: Optional[str] = None,
                  cookie_secret: Optional[str] = None,
                  ssl_cert: Optional[str] = None,
                  ssl_key: Optional[str] = None,
                  ssl: Optional[bool] = True,
                  certificate_source: Optional[str] = None,
                  custom_sans: Optional[List[str]] = None,
+                 email_domains: Optional[List[str]] = None,
                  allowlist_domains: Optional[List[str]] = None,
+                 ssl_insecure_skip_verify: Optional[bool] = False,
                  unmanaged: bool = False,
                  extra_container_args: Optional[GeneralArgList] = None,
                  extra_entrypoint_args: Optional[GeneralArgList] = None,
@@ -2603,12 +2606,19 @@ class OAuth2ProxySpec(ServiceSpec):
         #: The URL oauth2-proxy will redirect to after a successful login. If not provided
         # cephadm will calculate automatically the value of this url.
         self.redirect_url = redirect_url
+        #: OAuth scope specification.
+        # Default list of scopes will be used in case no scope is configured.
+        self.scope = scope
         #: The secret key used for signing cookies. Its length must be 16,
         # 24, or 32 bytes to create an AES cipher.
         self.cookie_secret = cookie_secret or self.generate_random_secret()
+        #: List of allowed email domains.
+        self.email_domains = email_domains
         #: List of allowed domains for safe redirection after login or logout,
         # preventing unauthorized redirects.
         self.allowlist_domains = allowlist_domains
+        #: Skip TLS verification for the OIDC provider. Use only in non-production environments.
+        self.ssl_insecure_skip_verify = ssl_insecure_skip_verify
         self.unmanaged = unmanaged
 
     def generate_random_secret(self) -> str:
@@ -2630,6 +2640,10 @@ class OAuth2ProxySpec(ServiceSpec):
         self._validate_url(self.oidc_issuer_url, "oidc_issuer_url")
         if self.redirect_url is not None:
             self._validate_url(self.redirect_url, "redirect_url")
+        if self.scope is not None:
+            self._validate_non_empty_string(self.scope, "scope")
+        if self.email_domains is not None:
+            self._validate_domain_name(self.email_domains, "email_domains")
         if self.https_address is not None:
             self._validate_https_address(self.https_address)
 
@@ -2647,6 +2661,22 @@ class OAuth2ProxySpec(ServiceSpec):
             if not all([result.scheme, result.netloc]):
                 raise SpecValidationError(f"Error parsing {field_name} field: Must be a valid URL.")
 
+    def _validate_domain_name(self, domains: Optional[List[str]], field_name: str) -> None:
+        from urllib.parse import urlparse
+        for domain in (domains or []):
+            try:
+                result = urlparse(f"http://{domain}")
+            except Exception as e:
+                raise SpecValidationError(
+                    f"Invalid {field_name}: {e}. Must be a valid domain name."
+                )
+            else:
+                if result.netloc != domain:
+                    raise SpecValidationError(
+                        f"Invalid {field_name}: '{domain}' is not a valid domain name. "
+                        f"Must be a valid domain (e.g., 'domain.test')."
+                    )
+
     def _validate_https_address(self, https_address: Optional[str]) -> None:
         from urllib.parse import urlparse
         result = urlparse(f'http://{https_address}')