Skip to content

dotAuth cleanup: SAML SYSTEM_HOST 500, silent discovery/config failures, and related fixes #38051

Open

Description

@swicken

Problem Statement

These findings come from end-to-end QA of dotAuth SSO and headless exchange against Keycloak 24.0.3 and 26.6.1 on dotcms/dotcms:26.10.12-01. The core flows work. The items below are validation gaps, silent failures and misleading UI messages that make dotAuth hard to configure and troubleshoot. One of them (item 1) can make /dotAdmin return 500. They are bundled here as one cleanup pass. The separate role-stripping defect, where an unassignable mapped role strips roles under SAML and OIDC, is tracked in its own issue.

Defects

1. SAML on SYSTEM_HOST without "SP endpoint hostname" makes /dotAdmin return 500.

  • How it is saved. toPayload omits sPEndpointHostname when it is blank, and validate() only requires the entity ID. SamlProtocolHandler and DotAuthResource then call appsAPI.saveSecrets directly, which skips the descriptor's required-field check. apps/dotsaml-config.yml marks sPEndpointHostname as required: true.
  • What breaks. Every anonymous GET /dotAdmin/ returns HTTP 500 (SamlWebInterceptor: NullPointerException, getSpEndpointHostname() is null in EndpointServiceImpl.spEndpointHostname). GET /api/v1/dotsaml/metadata/SYSTEM_HOST also returns 500 (NullPointerException "key is null" in SamlCoreServiceImpl.getCredential).
  • The classic Apps screen is not affected. POST /api/v1/apps/dotsaml-config/SYSTEM_HOST without the field is rejected with 400: "Param sPEndpointHostname is marked required in the descriptor but does not come with a value."
  • Re-saving needs an extra step. Once the hostname is set, saving it also needs regenerateKeypair=true, because a keypair was already stored.

2. OIDC discovery reports success when it fetched nothing. POST /api/v1/dotauth/discover/oidc returns HTTP 200 with every field empty in two cases. The UI then shows "Discovery succeeded — endpoints configured automatically" with blank endpoints.

  • The fetch fails. For example a TLS handshake error, and probably an untrusted certificate (not tested). The cause appears only in the log: CircuitBreakerUrl - Error accessing [...] due to [Unsupported or unrecognized SSL message].
  • The URL is the issuer rather than the well-known document (REST callers only; the UI appends /.well-known/openid-configuration).

3. A rejected or unbuildable SSO config fails silently. When OAuthAppConfig rejects a URL or the provider cannot be built, /dotAdmin just serves the local login page, and only a WARN explains why. A URL can be rejected because http is not allowed or because the host is private. The provider cannot be built when the discovery issuer does not match the configured issuer. When an http URL is rejected, the final WARN is misleading: Unable to build OAuth provider: OIDC issuerUrl is required, because the rejected issuer became null. The portlet keeps showing the green "SSO is live" banner throughout.

4. The discovery error dialog loses the server's message. With OAUTH_ALLOW_INSECURE_URLS=false, the server returns 400 {"message":"http:// URLs require OAUTH_ALLOW_INSECURE_URLS=true"}, but the "Bad request error" dialog body shows only http. The text appears to be cut at the first :.

5. The headless exchange and SSO overwrite each other's roles. With role behavior "Sync all", the headless exchange replaces the user's roles with the trusted IdP's provisioning (for example DOTCMS_FRONT_END_USER plus mapped roles). That removes DOTCMS_BACK_END_USER and the user's personal role; the next SSO login puts them back. The back-and-forth matches what "Sync all" means, but removing the personal role should not happen.

Related improvements

6. OIDC logout does not return users to dotCMS. /dotAdmin/logout sends the user to the IdP's end_session_endpoint with id_token_hint but no post_logout_redirect_uri; OIDCProvider.getLogoutUrl supports one, but the interceptor passes null. Users end on the IdP's "You are logged out" page.

7. Optional nonce per trusted headless IdP. /api/v1/dotauth/oauth/exchange requires nonce. Grants with no browser redirect, such as the password grant, cannot supply one; Keycloak drops nonce from that request. In this endpoint the nonce is a caller-supplied consistency check, not replay protection: one-time-use token fingerprinting and the audience check provide that. Suggestion: a per-IdP "Require nonce" toggle, on by default. When it is off, a nonce that is sent is still verified, and the audience and replay checks always stay on.

8. SAML SP endpoints are https-only. SP metadata always advertises https://<sPEndpointHostname>/dotsaml/login/<id> and the matching logout URL, with no setting for http. Confirm this is intended and document it, or add a setting.

Steps to Reproduce

Reproduced locally on dotcms/dotcms:26.10.12-01 with Keycloak 24.0.3 and 26.6.1 over plain http, with DOT_OAUTH_ALLOW_INSECURE_URLS=true unless stated otherwise.

  1. Item 1. PUT /api/v1/dotauth/sites/SYSTEM_HOST with protocol: SAML, valid IdP metadata and no sPEndpointHostname. Then GET /dotAdmin/ without a session returns 500.
  2. Item 2. In dotAuth, then System identity provider, then Configure SSO: enter https:// against an IdP that only serves http, then click Discover. The UI says "Discovery succeeded" with blank endpoints.
  3. Items 3 and 4. Save a working http OIDC config, restart dotCMS with DOT_OAUTH_ALLOW_INSECURE_URLS=false, open /dotAdmin (local login page, no redirect), then click Discover in the portlet (dialog body: "http").
  4. Item 5. Log in through SSO as user X, call POST /api/v1/dotauth/oauth/exchange for the same user, then compare GET /api/v1/roles/users/<email> before and after.
  5. Item 6. Log in through OIDC SSO and use the user menu's logout. The browser stops on the IdP's logged-out page.

Acceptance Criteria

  • dotAuth enforces the app descriptor's required fields when saving, the same way the classic Apps save does. Saving SAML on SYSTEM_HOST without an SP endpoint hostname is rejected with a clear message, and the UI marks the field as required for SAML.
  • /dotAdmin never returns 500 because of an incomplete SAML config. An unusable config is skipped with a logged error.
  • POST /api/v1/dotauth/discover/oidc returns an error status with the underlying cause when the fetch fails or the response has no issuer. The UI shows that error instead of "Discovery succeeded".
  • The discovery error dialog shows the server's full message.
  • When the saved SSO config cannot be used, the portlet does not show "SSO is live", and it shows why (rejected URL and reason, or issuer mismatch). The log names the real cause, not "issuerUrl is required".
  • Neither the headless exchange nor SSO role sync removes the user's personal role.
  • (Improvement) OIDC logout sends post_logout_redirect_uri when one is configured or derivable, and the user lands back on dotCMS.
  • (Improvement) A trusted headless IdP can be set to not require nonce. The default stays "required", and the audience and one-time-use checks are unaffected.
  • (Improvement) The https-only SAML endpoint behavior is either documented or made configurable.

dotCMS Version

Reproduced on dotcms/dotcms:26.10.12-01 (Evergreen). The same code paths are present on main as of 2026-10-15.

Severity

Medium - Some functionality impacted

Links

NA

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions