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
- Create a client secret for the service principal (az ad sp create-password ... or Azure portal) and configure it
- Or upload a certificate to the SP and configure GCM to use the certificate
- Verify the credential source (env var/config/helper) is actually populated where GCM reads it
- 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
- Always reset/assign a secret or certificate when creating a service principal
- Rotate and store SP credentials in a secret manager rather than config files
- Validate the SP record before invoking GCM
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
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
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)