vercel/ai · error

client_secret_basic authentication requires a client_secret

Error message

client_secret_basic authentication requires a client_secret

What it means

When the selected client authentication method is 'client_secret_basic' (HTTP Basic auth on the token endpoint), applyBasicAuth requires a client_secret to build the Base64 credentials. If clientSecret is falsy, it throws — a confidential client was registered for basic auth but no secret is available at token exchange/refresh time.

Source

Thrown at packages/mcp/src/tool/oauth.ts:839

      return;
    case 'client_secret_post':
      applyPostAuth(client_id, client_secret, params);
      return;
    case 'none':
      applyPublicAuth(client_id, params);
      return;
    default:
      throw new Error(`Unsupported client authentication method: ${method}`);
  }
}

function applyBasicAuth(
  clientId: string,
  clientSecret: string | undefined,
  headers: Headers,
): void {
  if (!clientSecret) {
    throw new Error(
      'client_secret_basic authentication requires a client_secret',
    );
  }

  const credentials = btoa(`${clientId}:${clientSecret}`);
  headers.set('Authorization', `Basic ${credentials}`);
}

/**
 * Applies POST body authentication (RFC 6749 Section 2.3.1)
 */
function applyPostAuth(
  clientId: string,
  clientSecret: string | undefined,
  params: URLSearchParams,
): void {
  params.set('client_id', clientId);
  if (clientSecret) {

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Ensure the clientSecret is present in the OAuthClientInformation used for the exchange (re-save with the secret from registration).
  2. If the client is truly public, re-register with token_endpoint_auth_method 'none' (or 'client_secret_post' with a secret if confidential).
  3. Check the persistence layer (provider.clientInformation()/saveClientInformation) isn't dropping the client_secret field.
  4. Re-run dynamic registration (invalidateCredentials('all')) to obtain a fresh client_id/secret pair.

Example fix

// before
provider.clientInformation = async () => ({ client_id: 'abc' }); // secret lost
// after
provider.clientInformation = async () => ({ client_id: 'abc', client_secret: await loadSecret(), token_endpoint_auth_method: 'client_secret_basic' });
Defensive patterns

Strategy: validation

Validate before calling

const info = await provider.clientInformation();
if (info && (info.token_endpoint_auth_method ?? 'client_secret_basic') === 'client_secret_basic' && !info.client_secret) {
  throw new Error('client_secret_basic selected but client_secret missing from stored client information');
}

Type guard

function hasSecretForBasicAuth(info: { client_id: string; client_secret?: string; token_endpoint_auth_method?: string }): info is { client_id: string; client_secret: string } {
  return (info.token_endpoint_auth_method ?? 'client_secret_basic') !== 'client_secret_basic' || typeof info.client_secret === 'string' && info.client_secret.length > 0;
}

Try / catch

try {
  await auth(provider, { serverUrl });
} catch (error) {
  if (String(error.message).includes('client_secret_basic authentication requires a client_secret')) {
    await provider.invalidateCredentials?.('all');
    await auth(provider, { serverUrl }); // re-register to obtain a secret
  }
}

Prevention

When it happens

Trigger: exchangeAuthorization or refreshAuthorization with auth method 'client_secret_basic' while clientInformation has only a client_id (e.g. registration returned no secret, or stored client info lost the secret, or a public client was misconfigured as confidential).

Common situations: Restoring persisted client information that dropped client_secret (storage layer stripping fields), registering against an AS that issues public clients but the code still selects basic auth, or a provider whose clientInformation() returns a partial object.

Understand the failure class

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/37e1139fce44db5f. Report an issue: GitHub.