Git Works in GitHub Desktop, So Why Does the Terminal Say "Authentication failed"?
Basic authentication with a PAT on an on-prem Azure DevOps Server
Also available in: Türkçe
Contents
I needed to create a new branch in a mobile project. A routine job: git fetch, git branch, move on. But the very first command gave me this:
| |
Another repo on the same server failed the same way. And on top of that, GitHub Desktop was connecting to the same server without any trouble. This post walks through the road to the fix, the wrong diagnoses I made along the way, and the one-line setting that finally solved it.
If you use an on-prem Azure DevOps Server (formerly TFS) and get this error in the terminal, the fix is most likely at the end.
Environment
- Windows 11, Git for Windows 2.52
- On-prem Azure DevOps Server, sign-in with domain accounts
- Git Credential Manager (GCM) 2.6.1, i.e.
credential.helper = manager - A Personal Access Token (PAT) for authentication
First finding: the server is up, the credentials are rejected
First I ruled out the network. I sent the server a simple request:
| |
That told me two things:
- It’s not a VPN/network problem. The server responds.
- The server offers four authentication methods:
Bearer,Basic,NegotiateandNTLM. Keep this list in mind, it’s the key to the whole story.
Wrong diagnosis #1: “You’re not on the team”
I noticed my name wasn’t on the project’s team, so I added myself as both a member and an administrator. It seemed logical, but the error stayed exactly the same.
I only understood the lesson later: in Azure DevOps, 401 means “you could not be authenticated”. If it were a permissions problem I would have seen a 403 or a message like TF401019. Besides, team membership doesn’t grant repo access by itself, that’s a separate security setting.
I also confirmed through the REST API that the PAT itself was valid and had the right permissions:
| |
So the repo was readable. Permissions weren’t the problem.
Wrong diagnosis #2: “Delete the old entry”
In Windows Credential Manager I saw an entry named GitHub - https://azuredevops.company.local/... and assumed it had gone stale. I deleted it, GitHub Desktop asked me to sign in on the next fetch, I entered my details and it worked. But the terminal still gave the same error.
That’s where I learned the second lesson: GitHub Desktop and Git in the terminal talk to the same server, but they store credentials in different places.
| Credential entry name | Used by | |
|---|---|---|
| GitHub Desktop | GitHub - https://server/... | Its own Git and its own credential handling |
| Git in the terminal | git:https://server | Git Credential Manager (GCM) |
The entry I deleted actually belonged to GitHub Desktop. Git in the terminal never used it. Deleting it didn’t affect the terminal at all, and it needlessly broke Desktop’s own entry.
A clue: turning on GCM tracing
GCM can produce a trace:
| |
The important part of the output was this:
| |
The trace shows two things:
- GCM can’t find a stored credential.
- Because the server supports Windows Integrated Authentication (automatic sign-in with your Windows account), GCM generates an empty username/password, hands it to Git and never asks me anything. On my machine (which isn’t joined to the domain) that automatic sign-in fails, and so does the fetch.
Fix 1: turning off Windows automatic sign-in
| |
This helped, but only halfway: now GCM opened a window and asked for a username and password. I entered them and… Authentication failed again.
Are the credentials right? Testing without Git
I took Git out of the picture and used curl to check whether the same username and PAT were really valid:
| |
I typed the PAT at the password prompt. Result: http=200.
The credentials were correct. With the same details curl worked and git didn’t. So the problem was in how Git’s HTTP client behaved, not in the credentials. When I disabled GCM completely (-c credential.helper=) and let Git prompt by itself, I got the same error, so GCM wasn’t the culprit either. Switching the SSL backend to openssl didn’t change anything.
Something in the prompt caught my eye along the way:
| |
The \ character showed up as %5C, and I thought “maybe this is it”. It wasn’t. That’s just Git displaying the username in URL form. The value sent to the server is the decoded DOMAIN\user.
The real cause: Git picks the “strongest” method
curl -u sends Basic authentication directly. Git works differently: it first sends the request without credentials, looks at the list of methods in the server’s 401 response and, following libcurl’s default behavior, picks the strongest method it sees. Since the list contains Negotiate and NTLM, one of those gets chosen instead of Basic. A PAT isn’t an NTLM password, so the server rejects it.
That’s exactly what the four-line WWW-Authenticate list I saw at the start was telling me.
The fix: forcing Basic up front
The http.proactiveAuth setting, added in Git 2.46, makes Git send the given authentication right away on the first request, without negotiating the method:
| |
I entered the username and the PAT. This time there was no fatal, the command finished quietly. It worked.
To make it permanent, only for that server:
| |
On the second command the GCM window opened once, asked for the username and PAT, and saved them. After that, the other repos on the same server worked too, without asking again.
Summary: the settings that are enough on their own
These were the two settings I needed. proactiveAuth is the one that actually solved the problem:
| |
allowWindowsAuth false stops GCM from generating an empty (WIA) credential without asking anything. proactiveAuth basic keeps Git from getting stuck on Negotiate/NTLM instead of Basic. Your setup may not need allowWindowsAuth, but if you see “Host supports WIA” in the trace output, setting both is the safe choice.
Lessons learned
- 401 and 403 aren’t the same thing. 401 is an authentication problem, 403 is a permissions problem. Adding myself to the team was a pointless detour.
- Figure out which layer the error is in. The difference between
curl(200) andgit(error) moved the problem from the “credentials” layer to the “Git’s HTTP behavior” layer. That single comparison was worth more than a hundred guesses. - GitHub Desktop working doesn’t mean the terminal will. They store credentials in different places and may use different Git binaries. Before deleting an entry, make sure you know which tool it belongs to.
- Turn on tracing. Seeing the “generating empty credential” line with
GCM_TRACE=1explained why GCM never asked me anything. - Look at the server’s
WWW-Authenticatelist. If you use Git with a PAT against an on-prem server that offers bothBasicandNegotiate/NTLM, you may run into this error again.
Quick checklist
If you get the same error, go through these in order:
- Use
curl -I <repo-url>to see the 401 and theWWW-Authenticateheaders. - Verify the credentials without Git:
curl.exe -u "DOMAIN\user" <repo-url>/info/refs?service=git-upload-pack. You’re looking for200. - Run
GCM_TRACE=1 git fetchto see what GCM is doing. If you see “generating empty credential”, addallowWindowsAuth false. - If it still fails, try
git -c credential.helper= -c http.proactiveAuth=basic fetch. - If that works, make
proactiveAuth basicpermanent for that server only.
I hope the next person fighting this error saves a few hours thanks to this post.
Note
The server and domain names in this post are anonymized. Never type your PAT in plain text on the command line or in a URL, use an interactive prompt or a credential manager instead.
Last updated: