【问题标题】:Replicate cURL Command Using Redirect and Cookies in .Net Core 3.1在 .Net Core 3.1 中使用重定向和 Cookie 复制 cURL 命令
【发布时间】:2020-07-14 19:44:20
【问题描述】:

这似乎是一个长远的目标。但是我已经看到了几个答案,表明在 .Net Core 应用程序中需要 cURL 时应该使用 HttpClient(和类似的)。

我有以下 cURL 命令(完美运行):

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"

这个命令的流程是这样的:

  1. 加载提供的 url (https://idp.domain.net/oauth2/authorize....)
  2. 获取 302 响应以重定向到 https://idp.domain.net/iwa-kerberos?state=state_guid_here
    • 因为有-L 选项,所以它遵循重定向
  3. 重定向响应带有 www-authenticate:Negotiate 标头的 401(未授权)。
  4. cURL 看到 www-authenticate:Negotiate 标头并从操作系统获取 Kerberos 令牌(由于 --negotiate-u 选项)。
  5. cURL 调用重定向 url (https://idp.domain.net/iwa-kerberos?state=state_guid_here) 并附加标头 Authorization: Negotiate <kerberos token here>
  6. 返回的 302 响应重定向到 https://idp.domain.net/commonauth?state=state_guid_here&iwaauth=1 并带有添加的 cookie
    • 由于-b 选项,cookie 被 cURL 拾取。
  7. cURL 调用重定向 url (https://idp.domain.net/commonauth?state=state_guid_here&iwaauth=1)使用上一步的 302 返回的 cookie
  8. 返回另一个 302 重定向。重定向到 https://idp.domain.net/oauth2/authorize?sessionDataKey=session_key_guid_here带有更多 cookie。 (由于-b 选项再次被选中。)
  9. 重定向到https://idp.domain.net/oauth2/authorize?sessionDataKey=session_key_guid_here 之后添加了cookie
  10. 另一个 302 重定向返回到 https://localhost:5001/?code=code_guid_here&session_state=session_state_here(添加了 cookie)。
  11. cURL 使用添加的 cookie 跟随重定向到 https://localhost:5001/?code=code_guid_here&session_state=session_state_here。
  12. https://localhost:5001/?code=code_guid_here&session_state=session_state_here 的内容返回到 cURL 命令行。

把这一切写出来,让它在 .Net 应用程序中工作似乎是一项艰巨的任务。但我想我会问它是否内置在某个地方的框架中。

是否有 .Net Core Framework 类(或类似的)可以让我在 C# 代码中重现此 cURL 命令?

注意:我可以通过调用 powershell 来做到这一点。这个问题是关于使用HttpClient

【问题讨论】:

  • 如果你尝试只使用HttpClient(和UseDefaultCredentials = true),那么它会在哪一步失败?
  • @Evk - 将UseDefaultCredentialsAllowAutoRedirect 都设置为true,它会进入第3 步并停止。

标签: c# curl .net-core .net-core-3.1


【解决方案1】:

我能够构建一个自定义目的方法来完成我需要的调用(以获取 OAuth 2 身份验证代码)。

Cookie 是自动添加的,但我是否添加了 CookieContainerUseCookies 设置似乎并不重要。

另外,UseDefaultCredentials 似乎也没有做任何事情。

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;
    }

}

如果您添加以下 NuGet 包,它将在 LinqPad 中运行:

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

我还有以下“使用”语句:

  • 系统

  • System.Collections

  • System.Collections.Generic

  • 系统数据

  • 系统诊断

  • 系统.IO

  • System.Linq

  • System.Linq.Expressions

  • 系统.反射

  • 系统文本

  • 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

  • 系统.Web

【讨论】:

    【解决方案2】:

    将 curl 标志转换为 HttpClient

    -L

    HttpClient 应该自动跟随重定向,因为HttpClientHandler.AllowAutoRedirect defaults to true

    --negotiate -u :

    HttpClient 将处理协商,如果您向其构造函数提供 HttpClientHandler 并为其提供凭据。由于您使用-u : 的默认Windows 凭据,您可以使用this answer 中的代码:

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

    -b ~/cookiejar.txt

    HttpClient 可以通过向其构造函数提供HttpClientHandlerCookieContainer 来存储cookie:

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

    这取决于HttpClientHandlerUseCookies = true, but it is true by default

    返回内容

    您可以使用HttpClient.GetAsyncHttpResponseMessage.Content 读取HttpClient 的响应

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

    一切结合

    如果我们显式设置上面引用的每个值,那么构造 HttpClient 并发出等效请求应该是这样的:

    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();
    

    【讨论】:

    • 我通过使用仅允许 www-authenticate: Negotiate 的 Windows 身份验证模拟默认的 ASP.NET Core API 模板对此进行了测试 - 如果上述方法不适合您,您能否确认您的应用程序如何它的行为可能会有所不同?
    • 感谢您的帮助。我将赏金奖励给你,但我将我的回答标记为“正确”答案,因为一些设置似乎对我不起作用。但我让它使用循环来检查一些响应!
    猜你喜欢
    • 1970-01-01
    • 2020-07-14
    • 2020-09-09
    • 1970-01-01
    • 1970-01-01
    • 2020-09-06
    • 2020-05-22
    • 1970-01-01
    • 2020-11-29
    相关资源
    最近更新 更多