Tencent/WeKnora · error

dynamic client registration returned an empty client_id

Error message

dynamic client registration returned an empty client_id

What it means

Raised when dynamic client registration completes without error but the handler returns an empty client_id, meaning the provider response lacked the required identifier. StartAuthorization treats this as a hard failure because the authorization URL cannot be built without a client_id.

Source

Thrown at internal/mcp/oauth_manager.go:110

	principal = principal.Normalize()
	if !principal.Valid() {
		return "", "", fmt.Errorf("principal context is required to authorize OAuth MCP service %s", service.ID)
	}

	h, err := m.newHandler(ctx, service, tenantID, principal, redirectURI)
	if err != nil {
		return "", "", err
	}

	// Register a client dynamically if we don't have one yet for this service.
	existing, _ := m.repo.GetClient(ctx, tenantID, service.ID)
	if existing == nil {
		if err := h.RegisterClient(ctx, clientRegistrationName); err != nil {
			return "", "", fmt.Errorf("dynamic client registration failed: %w", err)
		}
		clientID := h.GetClientID()
		if clientID == "" {
			return "", "", fmt.Errorf("dynamic client registration returned an empty client_id")
		}
		if err := m.repo.SaveClient(ctx, &types.MCPOAuthClient{
			TenantID:    tenantID,
			ServiceID:   service.ID,
			ClientID:    clientID,
			RedirectURI: redirectURI,
		}); err != nil {
			logger.GetLogger(ctx).Warnf("failed to persist MCP oauth client: %v", err)
		}
	}

	verifier, err := transport.GenerateCodeVerifier()
	if err != nil {
		return "", "", fmt.Errorf("failed to generate PKCE verifier: %w", err)
	}
	challenge := transport.GenerateCodeChallenge(verifier)
	state, err := transport.GenerateState()
	if err != nil {

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Inspect the provider's registration response for a non-standard body and fix parsing expectations
  2. Upgrade/patch the transport.OAuthHandler registration parsing to match the provider's response format
  3. Use a static, pre-registered client_id with the provider instead of dynamic registration
  4. Log the raw registration response to confirm what the provider returned
Defensive patterns

Strategy: try-catch

Validate before calling

// sanity-check the registration path with a dry-run against the provider
resp, err := http.Post(*svc.URL+"/register", "application/json", strings.NewReader(sampleReg))
if err == nil {
    var body map[string]any
    json.NewDecoder(resp.Body).Decode(&body)
    if _, ok := body["client_id"]; !ok {
        return fmt.Errorf("provider registration response lacks client_id; use static client")
    }
}

Type guard

func hasClientID(reg map[string]any) bool {
    id, ok := reg["client_id"]
    return ok && s, _ := id.(string); ok && s != ""
}

Try / catch

_, _, err := mgr.StartAuthorizationForService(ctx, svc, tenantID, principal, redirect, "")
if err != nil && strings.Contains(err.Error(), "empty client_id") {
    log.Warn("provider returned non-standard registration response; using static client")
    return startAuthorizationWithStaticClient(ctx, svc, config.StaticClientFor(svc.ID))
}
if err != nil { return err }

Prevention

When it happens

Trigger: Provider's /register endpoint returns 2xx with a body missing client_id (or the handler fails to parse it), immediately after h.RegisterClient succeeds.

Common situations: Provider returns a non-standard registration response (e.g. only registration_client_uri); misbehaving mock/idp in dev; response shape changed after a provider upgrade; JSON field name mismatch in parsing.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/3b5745174be088aa. Report an issue: GitHub.