Ken Muse

How Does Git Authentication Work?


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:

  1. Look for the GIT_ASKPASS environment variable
  2. If GIT_ASKPASS is not set, look for the configuration setting core.askPass.
  3. If neither of those are available, try to use the environment variable SSH_ASKPASS
  4. 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 call git-my-helper
  • helper = my-helper.sh will call git-my-helper.sh
  • helper = my-helper --x will call git-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 a get request.
  • erase: Erase a credential that matches a set of criteria. This is typically called in response to a failed authentication after a get 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.