Search code examples
c#.net-5libgit2libgit2sharp

LibGit2Sharp: How to push a local repo commit to Azure DevOps remote repo using a Personal Access Token inside a custom HTTP authentication header?


I am trying to push a commit I made on my local repository to a remote counterpart, hosted on a private Azure DevOps server, using LibGit2Sharp programmatically.

As per the Azure documentation, the HTTPS OAuth enabled Personal Access Token needs to sent with the request in a custom Authentication header as 'Basic' with the Base64 encoded token:

var personalaccesstoken = "PATFROMWEB";

    using (HttpClient client = new HttpClient()) {
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
            Convert.ToBase64String(Encoding.ASCII.GetBytes($":{personalaccesstoken}")));

        using (HttpResponseMessage response = client.GetAsync(
                    "https://dev.azure.com/{organization}/{project}/_apis/build/builds?api-version=5.0").Result) {
            response.EnsureSuccessStatusCode();
        }
    }

The LibGit2Sharp.CloneOptions class has a FetchOptions field which in turn has a CustomHeaders array that can be used to inject the authentication header during the clone operation, like the following (as mentioned in this issue):

        CloneOptions cloneOptions = new() {
        CredentialsProvider = (url, usernameFromUrl, types) => new UsernamePasswordCredentials {
            Username = $"{USERNAME}",
            Password = $"{ACCESSTOKEN}"
        },
        FetchOptions = new FetchOptions {
            CustomHeaders = new[] {
                $"Authorization: Basic {encodedToken}"
            }
        }
    };
    Repository.Clone(AzureUrl, LocalDirectory, cloneOptions);
    

And the clone process succeeds (I tested it as well as checked the source code :) )

However, the LibGit2Sharp.PushOptions does not have any such mechanism to inject authentication headers. I am limited to the following code:

PushOptions pushOptions = new()
        {
            CredentialsProvider = (url, usernameFromUrl, types) => new UsernamePasswordCredentials
            {
                Username = $"{USERNAME}",
                Password = $"{PASSWORD}"
            }
        };

This is making my push operation fail with the following message:

Too many redirects or authentication replays

I checked the source code for Repository.Network.Push() on Github.

public virtual void Push(Remote remote, IEnumerable<string> pushRefSpecs, PushOptions pushOptions)
    {
        Ensure.ArgumentNotNull(remote, "remote");
        Ensure.ArgumentNotNull(pushRefSpecs, "pushRefSpecs");

        // Return early if there is nothing to push.
        if (!pushRefSpecs.Any())
        {
            return;
        }

        if (pushOptions == null)
        {
            pushOptions = new PushOptions();
        }

        // Load the remote.
        using (RemoteHandle remoteHandle = Proxy.git_remote_lookup(repository.Handle, remote.Name, true))
        {
            var callbacks = new RemoteCallbacks(pushOptions);
            GitRemoteCallbacks gitCallbacks = callbacks.GenerateCallbacks();

            Proxy.git_remote_push(remoteHandle,
                                  pushRefSpecs,
                                  new GitPushOptions()
                                  {
                                      PackbuilderDegreeOfParallelism = pushOptions.PackbuilderDegreeOfParallelism,
                                      RemoteCallbacks = gitCallbacks,
                                      ProxyOptions = new GitProxyOptions { Version = 1 },
                                  });
        }
    }

As we can see above, the Proxy.git_remote_push method call inside the Push() method is passing a new GitPushOptions object, which indeed seems to have a CustomHeaders field implemented. But it is not exposed to a consumer application and is being instantiated in the library code directly!

It is an absolute necessity for me to use the LibGit2Sharp API, and our end-to-end testing needs to be done on Azure DevOps repositories, so this issue is blocking me from progressing further.

My questions are:

  1. Is it possible to use some other way to authenticate a push operation on Azure from LibGit2Sharp? Can we leverage the PushOptions.CredentialsProvider handler so that it is compatible with the auth-n method that Azure insists on?
  2. Can we cache the credentials by calling Commands.Fetch by injecting the header in a FetchOptions object before carrying out the Push command? I tried it but it fails with the same error.
  3. To address the issue, is there a modification required on the library to make it compatible with Azure Repos? If yes, then I can step up and contribute if someone could give me pointers on how the binding to the native code is made :)

Solution

  • I will provide an answer to my own question as we have fixed the problem.

    The solution to this is really simple; I just needed to remove the CredentialsProvider delegate from the PushOptions object, that is:

    var pushOptions = new PushOptions();
    

    instead of,

    PushOptions pushOptions = new()
            {
                CredentialsProvider = (url, usernameFromUrl, types) => new UsernamePasswordCredentials
                {
                    Username = $"{USERNAME}",
                    Password = $"{PASSWORD}"
                }
            };
    

    ¯\(ツ)

    I don't know why it works, but it does. (Maybe some folks from Azure can clarify it to us.)