@@ -24,6 +24,7 @@ import type { IdPAdapter } from './idp/interface'
2424import { isSmartLaunch , isStandaloneLaunch , parseScopes } from './smart-scopes'
2525import { verifyLaunchCode , type LaunchCodeServiceOptions } from './launch-code'
2626import { isRedirectUriRegistered , type GetRegisteredRedirectUris } from './redirect-uri'
27+ import { isCimdClientId } from './cimd'
2728
2829export interface AuthorizeInterceptorDeps {
2930 config : SmartProxyConfig
@@ -136,16 +137,39 @@ export async function handleAuthorize(
136137 // SmartProxyConfig.interceptedResourceUrls.
137138 const targetsInterceptedResource = ! ! aud && ( config . interceptedResourceUrls ?? [ ] ) . includes ( aud )
138139
139- // NB: this is uniform across registration methods on purpose. A CIMD client
140- // (`client_id` is an https URL) has no record in the IdP, so an earlier version
141- // excluded it here — which took the beta MCP connector down, because
142- // `getRegisteredRedirectUris` found nothing and the fail-closed check below
143- // rejected every authorize with 400 before a login page rendered. Excluding it
144- // the other way (skip interception for CIMD) merely hid the same gap and handed
145- // an authorization-server MUST back to the IdP. The resolver behind
146- // `getRegisteredRedirectUris` now understands both registration methods, so this
147- // does not need to know which one produced the client.
148- const shouldIntercept = ( smartLaunch || targetsInterceptedResource ) && ! ! params . redirect_uri
140+ let shouldIntercept = ( smartLaunch || targetsInterceptedResource ) && ! ! params . redirect_uri
141+
142+ // ── Only intercept what we can validate ───────────────────────────────
143+ // Interception rewrites redirect_uri to the proxy callback, which takes the
144+ // RFC 6749 §10.6 check away from the IdP and gives it to us. We may only do
145+ // that when we can actually perform it.
146+ //
147+ // For a CIMD client (`client_id` is an https URL) the allowlist lives in a
148+ // document on the CLIENT'S OWN HOST, and fetching it can fail for reasons that
149+ // say nothing about the request: a bot-protection interstitial in front of that
150+ // host, egress restrictions, an outage. Treating an unreadable document as "no
151+ // registered URIs" would reject a legitimate authorize request that, before any
152+ // of this existed, passed through to the IdP and worked — the IdP resolves CIMD
153+ // itself and can validate what we could not read.
154+ //
155+ // So: resolve first, and only take over when the document actually answered.
156+ // Otherwise stand aside. The cost is that such clients keep the IdP's `iss`
157+ // instead of ours, which is a conformance gap; rejecting them outright would be
158+ // an outage, and an outage is worse.
159+ if ( shouldIntercept && isCimdClientId ( params . client_id ) && deps . getRegisteredRedirectUris ) {
160+ let resolved : string [ ]
161+ try {
162+ resolved = await deps . getRegisteredRedirectUris ( params . client_id ! )
163+ } catch {
164+ resolved = [ ]
165+ }
166+ if ( resolved . length === 0 ) {
167+ logger ?. warn ( 'CIMD metadata document unavailable — passing through to the IdP unintercepted' , {
168+ clientId : params . client_id ,
169+ } )
170+ shouldIntercept = false
171+ }
172+ }
149173
150174 // ── Validate redirect_uri against the client's registered URIs ────────
151175 // RFC 6749 §3.1.2.3 / §10.6: reject any redirect_uri that is not an EXACT
0 commit comments