There’s an interesting aspect to Git that doesn’t get a lot of attention: how it authenticates with remote repositories. Most people are familiar with the basic authentication methods, like SSH keys, Git Credential Manager or tokens. Turns out there’s a lot more to the story. Like most things in Git, it’s a configurable component often implemented with binaries or scripts that are executed. In this post, we’ll explore that in more detail.
The credential helper
The process begins when you try to access a remote repository. Git will then need credentials to authenticate with the repository. Ideally, it will store those credentials to avoid asking for them again. This is where the credential helper comes into play. A credential helper is a program or script that can prompt for, store, retrieve, or erase credentials. If it is configured, Git will use it to manage credentials.
To configure a credential helper, you can use the git config
command and specify whether this is a system-wide (--system
), user-wide (--global
) or repository-specific (--local
) configuration. For example, to use a script or executable called my-helper
as a credential helper, you would run the following command:
1git config --global credential.helper my-helper
This creates a Git config entry such as:
1[credential]
2 helper = my-helper
Of course, Git also lets you configure a credential helper for a special URL. This is useful if you have multiple environments, such as organizations using a mix of GitHub Enterprise Cloud with Enterprise Managed Users (EMU) and repositories using personal accounts. To do this, you can specify the URL between credential
and helper
. For example, to only use my-helper
with GitHub, you could run:
1git config --global credential.https://github.com.helper my-helper
The Git config entry is instead:
1[credential "https://github.com"]
2 helper = my-helper
One thing to know about this process: by default, if there are multiple matching credential helpers, Git will use the last one it finds. For example:
1[credential "https://github.com"]
2 helper = my-helper
3[credential "https://github.com"]
4 helper = my-other-helper
Would always use my-other-helper
for the website https://github.com
.
The special case of paths
What happens if you want to use a credential helper for parts of a path? For example, let’s assume I need a separate login for the organization myorg
. That means my URL needs to be anything that starts with https://github.com/myorg
. If you run:
1git config --global credential.https://github.com/myorg.helper my-helper
The credential helper behavior may not be quite what you expect. Not only will Git match all requests that start with https://github.com/myorg
, it will also match the host name https://github.com
. That means that https://github.com/myotherorg
will also be matched. It turns out that Git actually only cares about the protocol and hostname by default. It ignores the rest of the path.
It turns out there’s another setting, useHttpPath
, that you can change to modify this logic. If this value is set to true, Git will match the entire URL, including the path. That said, it will still match the host name. However, if there is a more specific match – such as a credential helper configured for https://github.com
. So this configuration would work to match a single organization, but fall back to a different helper for all other requests to GitHub:
1git config --global credential.https://github.com/myorg.helper my-helper
2git config --global credential.https://github.com/myorg.useHttpPath true
3
4git config --global credential.https://github.com.helper my-default-helper
How are missing helpers handled?
When a credential helper is not provided, Git uses a fallback approach to find a program to use:
- Look for the
GIT_ASKPASS
environment variable - If
GIT_ASKPASS
is not set, look for the configuration settingcore.askPass
. - If neither of those are available, try to use the environment variable
SSH_ASKPASS
- If that’s not available, use built-in logic to prompt for credentials
It will then prompt the user for credentials, reading the details from the standard input.
What’s really going on?
When a credential is needed, Git tries to fill the credential. I discussed some of this in my post on
troubleshooting Git authentication issues. It calls git credential fill
to retrieve the credentials. In turn, that calls the credential helper. If the helper has an absolute path, Git will invoke it directly. If the path is not absolute, Git will invoke git-${helper}
and rely on the shell to execute that program. That means:
helper = my-helper
will callgit-my-helper
helper = my-helper.sh
will callgit-my-helper.sh
helper = my-helper --x
will callgit-my-helper --x
helper = /bin/my-helper --x
will call/bin/my-helper --x
When the credential helper is called, it receives a single command line argument indicating what needs to be done.
get
: Retrieve a credential based on a set of criteria.store
: Store the credential details that are being provided. This is typically called in response to a successful authentication after aget
request.erase
: Erase a credential that matches a set of criteria. This is typically called in response to a failed authentication after aget
request.
If the helper cannot process that command, it should ignore it. For example, if the helper is a script that only supports get
, it should ignore store
and erase
commands.
Git will provide the criteria to the credential helper via standard input as a series of key-value pairs. For example, a request to retrieve the credentials for https://github.com/myorg/myrepo.git
would have this provided:
1protocol=https
2host=github.com
3path=myorg/myrepo.git
4wwwauth[]=Basic
5realm="GitHub"
The path
is only provided if the useHttpPath
setting is set to true. The other parameters are populated based on the authentication request. Git expects the response to contain values for username
and password
in the same format:
1username=myuser
2password=mypassword
If there is no response or the script fails to execute, Git will assume the credentials are not available and prompt the user. If it receives the credentials, it will try to authenticate with them. It will then call store
to save the credentials if the authentication was successful. If the authentication fails, it will call erase
to remove the credentials. In both cases, it will include the username
and password
in the request.
1protocol=https
2host=github.com
3username=myuser
4password=mypassword
5path=myorg/myrepo.git
6wwwauth[]=Basic
7realm="GitHub"
If you’re interested in seeing this in action, you can use the following script to write the credentials to standard error.
1#!/bin/bash
2
3COMMAND=$1
4readarray PARAMETERS
5printf "\n ---- ${COMMAND} ----\n" >&2
6for PARAM in ${PARAMETERS[@]}
7do
8 printf "${PARAM}\n" >&2
9done
10printf "\n---------------\n" >&2
Using the helpers
I hope this helps you to have a better understanding of how Git authentication works and demystifies some of the magic behind it. Out of the box, Git provides a few credential helpers. These are referenced in the
Git documentation. In addition, you can use the
Git Credential Manager to manage credentials (for macOS and Linux, it adds /usr/local/share/gcm-core/git-credential-manager
). If you use the
GitHub CLI, it can also configure a credential helper for you (credential.helper=gh auth git-credential
). And of course, you can always write your own. That’s the beauty of Git: it’s extensible and flexible. You can use it to meet your needs.