Make OAuth errors actionable when DCR is disabled

What does this MR do and why?

Makes the OAuth errors a client sees actionable after an administrator disables dynamic client registration (DCR), instead of leaving MCP/AI tools (Claude Code, Kiro, etc.) with cryptic OAuth errors and no path to recover.

When DCR is disabled, the cleanup worker (Authn::OauthApplications::CleanupDynamicApplicationsWorker, added in !248615 (merged)) destroys the dynamically registered OAuth applications. Three different re-auth paths then fail, and none of these clients recover gracefully on their own:

  • Stale client (token endpoint) — a client reuses a cached client_id that no longer resolves to an application, so POST /oauth/token returns a bare invalid_client ("Client authentication failed due to unknown client ...").
  • Fresh client (register endpoint) — a client tries to register from scratch, so POST /oauth/register returns 403 access_denied that previously said only "Dynamic client registration is disabled on this instance".
  • Interactive re-auth (authorize page) — a client sends the user to /oauth/authorize with a client_id that no longer resolves to an application. Previously the error page dumped Doorkeeper's raw error_description in a <pre> block with no guidance.

All three now surface an actionable message that points to the docs for reusing a single pre-registered OAuth application.

Implementation

  • Oauth::TokensController — a before_action returns an actionable invalid_client (HTTP 401, matching Doorkeeper). It only fires once the client_id is shown not to reference any application (Authn::OauthApplication.exists_for_uid?), so a known client_id with a bad secret still falls through to Doorkeeper.
  • Oauth::DynamicRegistrationsController — the existing 403 error_description is extended with the same guidance and link.
  • Oauth::AuthorizationsController — a new explain_missing_dynamic_client before_action (on :new/:create) renders doorkeeper/authorizations/error with an actionable override message and docs link. It fires only when DCR is disabled, a client_id is present, and it does not resolve to an application, so normal authorize flows are untouched.
  • app/views/doorkeeper/authorizations/error.html.haml — when given an override, the view now renders the actionable message plus a clickable docs link; otherwise it falls back to the existing raw error_description, now wrapped with gl-whitespace-pre-wrap gl-break-anywhere so long messages/URLs wrap instead of overflowing.
  • All links use help_page_url, so they point at the instance's own /help and are correct for GitLab Self-Managed and relative-URL installs rather than hardcoded to docs.gitlab.com.
  • The new user-facing string is added to locale/gitlab.pot for i18n.
  • Every path is reached only when DCR is disabled. When DCR is enabled (the default, including GitLab.com) behavior is unchanged.

Note on certainty

The endpoints cannot prove a given client_id was a cleaned-up DCR application versus one that never existed, so the copy is phrased conditionally ("If it was registered via dynamic client registration ..."). The remediation — pre-register an application and configure the client with its client ID — is correct regardless of the exact cause.

How to set up and validate locally

Must be running GDK in Self-Managed mode (non-SaaS mode).

  1. Disable DCR via the API:

    curl --request PUT --header "PRIVATE-TOKEN: <admin_token>" \
      --url "https://gitlab.example.com/api/v4/application/settings?dynamic_client_registration_enabled=false"
  2. POST /oauth/token with a client_id that does not exist — expect 401 with error: invalid_client and an error_description linking to the reuse-a-single-OAuth-application docs.

  3. POST /oauth/register — expect 403 with error: access_denied and the same actionable error_description.

  4. Visit /oauth/authorize?...&client_id=<nonexistent> in the browser — expect the error page to show the actionable message and a clickable docs link rather than a raw error dump.

  5. Repeat all three with DCR enabled — expect the standard errors with no docs link (token endpoint) / the endpoints working normally (register, authorize).

Edited by Jessie Young

Merge request reports

Loading
Loading