git-ecosystem/git-credential-manager · error · ArgumentException

Service principal ' ' must have either a certificate or…

Error message

Service principal '{sp.Id}' must have either a certificate or client secret.

What it means

When authenticating an Entra ID service principal via a confidential client, MSAL requires a credential: either a client certificate or a client secret. This guard fires when the service-principal record has neither, making token acquisition impossible.

Solutions

  1. Create a client secret for the service principal (az ad sp create-password ... or Azure portal) and configure it
  2. Or upload a certificate to the SP and configure GCM to use the certificate
  3. Verify the credential source (env var/config/helper) is actually populated where GCM reads it
  4. Confirm you are targeting the correct app/SP ID — a wrong ID can look credential-less

Example fix

# before: SP with no credential configured
az ad sp create --id ...   # no secret/cert
# after
az ad sp credential reset --id <sp-id>   # generates a client secret, configure it for GCM
Defensive patterns

Strategy: validation

Validate before calling

if (string.IsNullOrEmpty(sp.ClientSecret) && sp.ClientCertificate is null)
    throw new ArgumentException("Service principal needs a client secret or certificate before authentication.", nameof(sp));

Type guard

bool HasCredential(ServicePrincipal sp) => !string.IsNullOrEmpty(sp.ClientSecret) || sp.ClientCertificate is not null;

Try / catch

try { /* SP auth */ }
catch (ArgumentException ex) when (ex.Message.Contains("must have either a certificate or client secret")) { /* guide user to create a credential */ }

Prevention

When it happens

Trigger: GetTokenForServicePrincipalAsync builds a ConfidentialClientApplication; sp.ClientCertificate is null so the secret branch runs, but sp.ClientSecret is also null/empty, hitting the ArgumentException before builder.Build().

Common situations: Service principal created in Azure without generating a client secret; secret expired and removed; certificate never uploaded; configuration file/helper returning an SP record with credentials omitted (e.g. missing GCM_SECRET or cert settings).

Understand the failure class

Related errors


AI-assisted analysis of git-ecosystem/git-credential-manager@e8ce762cd0 (2026-09-11). Data as JSON: /api/errors/4a619a1db2ecdcbe. Report an issue: GitHub.

Appendix: source

Thrown at src/Core/Authentication/Entra/EntraAuthentication.ConfidentialClient.cs:33

        Context.Trace.WriteLine($"Creating confidential client for service principal '{sp.Id}' in tenant '{sp.TenantId}'...");
        var builder = ConfidentialClientApplicationBuilder.Create(sp.Id)
            .WithTenantId(sp.TenantId)
            .WithHttpClientFactory(_httpFactory)
            .WithTraceLogging(Context);

        if (sp.Certificate is not null)
        {
            Context.Trace.WriteLine($"Using service principal certificate: {sp.Certificate.Thumbprint}");
            builder.WithCertificate(sp.Certificate);
        }
        else if (!string.IsNullOrWhiteSpace(sp.ClientSecret))
        {
            Context.Trace.WriteLineSecrets("Using service principal secret: {0}", [sp.ClientSecret]);
            builder.WithClientSecret(sp.ClientSecret);
        }
        else
        {
            throw new ArgumentException($"Service principal '{sp.Id}' must have either a certificate or client secret.", nameof(sp));
        }

        Context.Trace.WriteLine($"SendX5C is '{sp.SendX5C}'");

        IConfidentialClientApplication app = builder.Build();
        await RegisterCacheAsync(app);

        Context.Trace.WriteLine($"Acquiring token for service principal with scopes '{string.Join(", ", scopes)}'...");
        AuthenticationResult result = await app.AcquireTokenForClient(scopes)
            .WithSendX5C(sp.SendX5C)
            .ExecuteAsync(ct);

        return AuthResult.FromMsalResult(result);
    }

    public async Task<IEntraAuthenticationResult> GetTokenForManagedIdentityAsync(
        string resource, ManagedIdentity mi, CancellationToken ct = default)
    {

View on GitHub (pinned to e8ce762cd0)