Replicate cURL Command Using Redirect and Cookies in .Net Core 3.1

Viewed 1002

This one seems a long shot. But I have seen several answers that indicate that HttpClient (and similar) should be used when cURL is needed in a .Net Core Application.

I have the following cURL command (that works perfectly):

curl -v -L --negotiate -u : -b ~/cookiejar.txt  "https://idp.domain.net/oauth2/authorize?scope=openid&response_type=code&redirect_uri=https://localhost:5001&client_id=client_id_here"

The flow of this command goes like this:

  1. Loads the provided url (https://idp.domain.net/oauth2/authorize....)
  2. Gets a 302 response to redirect to https://idp.domain.net/iwa-kerberos?state=state_guid_here
    • Because the -L option is there, it follows the redirect
  3. The redirect responds with a 401 (Unauthorized) with a www-authenticate:Negotiate header.
  4. cURL sees the www-authenticate:Negotiate header and gets a Kerberos token from the operating system (because of the --negotiate and -u options).
  5. cURL calls the redirect url (https://idp.domain.net/iwa-kerberos?state=state_guid_here) with an additional header of Authorization: Negotiate <kerberos token here>.
  6. A response of 302 returned redirecting to https://idp.domain.net/commonauth?state=state_guid_here&iwaauth=1 with an added cookie
    • Because of the -b option, the cookie is picked up by cURL.
  7. cURL calls the redirect url (https://idp.domain.net/commonauth?state=state_guid_here&iwaauth=1) with the cookie returned in the 302 of the previous step.
  8. Another 302 redirect is returned. Redirecting to https://idp.domain.net/oauth2/authorize?sessionDataKey=session_key_guid_here with more cookies. (Again picked up because of the -b option.)
  9. The redirect to https://idp.domain.net/oauth2/authorize?sessionDataKey=session_key_guid_here is followed with the added cookies.
  10. Another 302 redirect is returned to https://localhost:5001/?code=code_guid_here&session_state=session_state_here (with an added cookie).
  11. cURL folows the redirect to https://localhost:5001/?code=code_guid_here&session_state=session_state_here with the added cookie.
  12. Contents of https://localhost:5001/?code=code_guid_here&session_state=session_state_here are returned to the cURL command line.

Writing this all out, it seems like a serious undertaking to get this to work in a .Net Application. But I figured I would ask in case it is built in to the framework somewhere.

Is there a .Net Core Framework class (or similar) that can allow me to reproduce this cURL command in C# code?

NOTE: I am able to do it by calling out to powershell. This question is about doing it with HttpClient.

2 Answers

Converting curl flags to HttpClient

-L

HttpClient should automatically follow redirects because HttpClientHandler.AllowAutoRedirect defaults to true.

--negotiate -u :

HttpClient will handle negotiation if you supply its constructor with a HttpClientHandler that supplies it with credentials. Since you're using default Windows credentials with -u :, you can use the code from this answer:

var client = new HttpClient(new HttpClientHandler() { UseDefaultCredentials = true });

-b ~/cookiejar.txt

HttpClient can store cookies by supplying its constructor with an HttpClientHandler with a CookieContainer:

var client = new HttpClient(new HttpClientHandler() { CookieContainer = new CookieContainer() })

This depends on the HttpClientHandler having UseCookies = true, but it is true by default.

Returning Contents

You can use HttpClient.GetAsync and HttpResponseMessage.Content to read the response of the HttpClient

var response = await client.GetAsync("");
var content = await response.Content.ReadAsStringAsync();

Everything Combined

Here's what constructing HttpClient and making an equivalent request should look like if we explicitly set every value referenced above:

var client = new HttpClient(
    new HttpClientHandler()
    {
        // -L
        AllowAutoRedirect = true,

        // --negotiate -u :
        UseDefaultCredentials = true,

        // -b ~/cookiejar.txt
        CookieContainer = new CookieContainer(),
        UseCookies = true
    }
);

var response = await client.GetAsync("https://idp.domain.net/oauth2/authorize?scope=openid&response_type=code&redirect_uri=https://localhost:5001&client_id=client_id_here");
var content = await response.Content.ReadAsStringAsync();

I was able to build a custom purpose method that completed the call I needed (to get an OAuth 2 Auth Code).

The Cookies got added automatically, but it did not seem to matter if I added the CookieContainer and UseCookies settings or not.

Also, UseDefaultCredentials did not seem to do anything either.

async Task Main()
{   
    var services = new ServiceCollection();
    services.AddHttpClient("OAuthClient").ConfigurePrimaryHttpMessageHandler(() => new AuthenticationHandler());;
    var serviceProvider = services.BuildServiceProvider();  
    var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();   
    
    var authCodeUrl = "https://idp.domain.net/oauth2/authorize?scope=openid&response_type=code&redirect_uri=https://localhost:5001&client_id=client_id_here";
    var authNegotiator = new AuthenticaitonNegotiator(httpClientFactory);
    
    var authCode = await authNegotiator.GetAuthorizationCodeViaKerberosIwa(authCodeUrl);    
    Console.WriteLine(authCode);
}


public class AuthenticaitonNegotiator
{
    private IHttpClientFactory httpClientFactory;
    
    public AuthenticaitonNegotiator(IHttpClientFactory httpClientFactory)
    {
        this.httpClientFactory = httpClientFactory; 
    }
    
    public async Task<string> GetAuthorizationCodeViaKerberosIwa(string authCodeUrl)
    {
        var kerberosToken = GetKerberosTokenViaIwa();
        var authCode = await GetAuthorizationCodeViaKerberos(authCodeUrl, kerberosToken);
        return authCode;
    }

    public async Task<string> GetAuthorizationCodeViaKerberosCredsAsync(string authCodeUrl, string username, string password)
    {
        var kerberosToken = await GetKerberosTokenViaCredsAsync(username, password);
        var authCode = await GetAuthorizationCodeViaKerberos(authCodeUrl, kerberosToken);
        return authCode;
    }

    public async Task<string> GetAuthorizationCodeViaKerberos(string authCodeUrl, string kerberosToken)
    {
        var httpClient = httpClientFactory.CreateClient("OAuthClient");
        
        var done = false;
        string currentUrl = authCodeUrl;
        string responseText = "";
        bool wasSuccessful = false;
        while (!done)
        {           
            var response = await httpClient.GetAsync(currentUrl);
            responseText = await response.Content.ReadAsStringAsync();
            // Reset the authenticaiton header if it was set.  (It gets set as needed on each iteration.)
            httpClient.DefaultRequestHeaders.Authorization = null;          

            if (response.StatusCode == HttpStatusCode.Unauthorized
                && response.Headers.Any(x => x.Key == "WWW-Authenticate" && x.Value.Contains("Negotiate")))
            {
                currentUrl = response.RequestMessage.RequestUri.AbsoluteUri;                                        
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Negotiate", kerberosToken);
            }
            else if (response.StatusCode == HttpStatusCode.Redirect)
            {
                var redirectUri = response.Headers.Location;
                var query = HttpUtility.ParseQueryString(redirectUri.Query);
                var code = query.Get("code");
                if (code == null)
                {
                    currentUrl = redirectUri.AbsoluteUri;
                }
                else
                {
                    // If this is the last redirect where we would send to the callback, just grab the auth code.
                    // This saves us from needing to host a service to handle the callback.
                    responseText = code;
                    done = true;
                    wasSuccessful = true;
                }
            }
            else
            {
                done = true;
                wasSuccessful = false;
            }
        }
        
        if (wasSuccessful == false) 
        {
            throw new ApplicationException($"Failed to retrive authorization code: \r\n {responseText}");
        }
        
        return responseText;        
    }

    public async Task<String> GetKerberosTokenViaCredsAsync(string username, string password)
    {
        var client = new KerberosClient();
        var kerbCred = new KerberosPasswordCredential(username, password, "YourDomain.net");
        await client.Authenticate(kerbCred);
        var ticket = await client.GetServiceTicket("http/ServerToGetTheKerberosToken.YourDomain.net");
        return Convert.ToBase64String(ticket.EncodeGssApi().ToArray());
    }

    public string GetKerberosTokenViaIwa()
    {
        string token = "";
        using (var context = new SspiContext($"http/ServerToGetTheKerberosToken.YourDomain.net", "Negotiate"))
        {
            var tokenBytes = context.RequestToken();

            token = Convert.ToBase64String(tokenBytes);
        }
        return token;
    }
}


public class AuthenticationHandler : HttpClientHandler
{
    public AuthenticationHandler()
    {       
        // cURL Equivilant: -L
        AllowAutoRedirect = true;
        MaxAutomaticRedirections = 100;

        // cURL Equivilant: --negotiate -u :
        UseDefaultCredentials = true;

        // cURL Equivilant: -b ~/cookiejar.txt
        CookieContainer = new CookieContainer();
        UseCookies = true;
    }

}

This runs in LinqPad if you add the following NuGet Packages:

  • Kerberos.NET
  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.Http

I also had the following "using" statements:

  • System

  • System.Collections

  • System.Collections.Generic

  • System.Data

  • System.Diagnostics

  • System.IO

  • System.Linq

  • System.Linq.Expressions

  • System.Reflection

  • System.Text

  • System.Text.RegularExpressions

  • System.Threading

  • System.Transactions

  • System.Xml

  • System.Xml.Linq

  • System.Xml.XPath

  • Kerberos.NET

  • Kerberos.NET.Client

  • Kerberos.NET.Credentials

  • Kerberos.NET.Entities

  • Kerberos.NET.Win32

  • Microsoft.Extensions.DependencyInjection

  • Microsoft.Extensions.Http

  • System.Net

  • System.Net.Http

  • System.Net.Http.Headers

  • System.Threading.Tasks

  • System.Web

Related