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_idthat no longer resolves to an application, soPOST /oauth/tokenreturns a bareinvalid_client("Client authentication failed due to unknown client ..."). - Fresh client (register endpoint) — a client tries to register from
scratch, so
POST /oauth/registerreturns403 access_deniedthat previously said only "Dynamic client registration is disabled on this instance". - Interactive re-auth (authorize page) — a client sends the user to
/oauth/authorizewith aclient_idthat no longer resolves to an application. Previously the error page dumped Doorkeeper's rawerror_descriptionin 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— abefore_actionreturns an actionableinvalid_client(HTTP 401, matching Doorkeeper). It only fires once theclient_idis shown not to reference any application (Authn::OauthApplication.exists_for_uid?), so a knownclient_idwith a bad secret still falls through to Doorkeeper.Oauth::DynamicRegistrationsController— the existing 403error_descriptionis extended with the same guidance and link.Oauth::AuthorizationsController— a newexplain_missing_dynamic_clientbefore_action(on:new/:create) rendersdoorkeeper/authorizations/errorwith an actionable override message and docs link. It fires only when DCR is disabled, aclient_idis 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 rawerror_description, now wrapped withgl-whitespace-pre-wrap gl-break-anywhereso long messages/URLs wrap instead of overflowing.- All links use
help_page_url, so they point at the instance's own/helpand 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.potfor 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).
-
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" -
POST /oauth/tokenwith aclient_idthat does not exist — expect401witherror: invalid_clientand anerror_descriptionlinking to the reuse-a-single-OAuth-application docs. -
POST /oauth/register— expect403witherror: access_deniedand the same actionableerror_description. -
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. -
Repeat all three with DCR enabled — expect the standard errors with no docs link (token endpoint) / the endpoints working normally (register, authorize).
Related
- Backend follow-up to !248615 (merged)
- Related work item #601438 (closed)