[使用HttpListener的Windows桌面应用程序的OAuth 2.0授权

问题描述 投票:0回答:2

我正在用C#编写带有外部身份验证(Windows,Facebook)的Windows桌面应用程序。

我正在使用HttpListener允许用户通过具有ASP.NET Web API的外部身份验证服务来获取Barer令牌,但对此需要管理员特权,并且我希望在不使用管理模式的情况下运行。

我的参考是Sample Desktop Application for Windows

这是来自C#的外部身份验证提供程序的最佳实践吗?还是有另一种方法?

这是我的代码,用于通过外部提供程序获取Barer令牌:

public static async Task<string> RequestExternalAccessToken(string provider)
{
    // Creates a redirect URI using an available port on the loopback address.
    string redirectURI = string.Format("http://{0}:{1}/", IPAddress.Loopback, GetRandomUnusedPort());

    // Creates an HttpListener to listen for requests on that redirect URI.
    var http = new HttpListener();
    http.Prefixes.Add(redirectURI);
    http.Start();

    // Creates the OAuth 2.0 authorization request.
    string authorizationRequest = Properties.Settings.Default.Server
        + "/api/Account/ExternalLogin?provider="
        + provider
        + "&response_type=token&client_id=desktop"
        + "&redirect_uri="
        + redirectURI + "?";

    // Opens request in the browser.
    System.Diagnostics.Process.Start(authorizationRequest);

    // Waits for the OAuth authorization response.
    var context = await http.GetContextAsync();

    // Sends an HTTP response to the browser.
    var response = context.Response;
    string responseString = string.Format("<html><head></head><body></body></html>");
    var buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
    response.ContentLength64 = buffer.Length;
    var responseOutput = response.OutputStream;
    Task responseTask = responseOutput.WriteAsync(buffer, 0, buffer.Length).ContinueWith((task) =>
    {
        responseOutput.Close();
        http.Stop();
        Console.WriteLine("HTTP server stopped.");
    });

    // Checks for errors.
    if (context.Request.QueryString.Get("access_token") == null)
    {
        throw new ApplicationException("Error connecting to server");
    }

    var externalToken = context.Request.QueryString.Get("access_token");

    var path = "/api/Account/GetAccessToken";

    var client = new RestClient(Properties.Settings.Default.Server + path);
    RestRequest request = new RestRequest() { Method = Method.GET };
    request.AddParameter("provider", provider);
    request.AddParameter("AccessToken", externalToken);
    request.AddHeader("Content-Type", "application/x-www-form-urlencoded");

    var clientResponse = client.Execute(request);

    if (clientResponse.StatusCode == HttpStatusCode.OK)
    {
        var responseObject = JsonConvert.DeserializeObject<dynamic>(clientResponse.Content);

        return responseObject.access_token;
    }
    else
    {
        throw new ApplicationException("Error connecting to server", clientResponse.ErrorException);
    }
}
c# asp.net-web-api oauth-2.0 google-authentication httplistener
2个回答
0
投票

[我不了解Facebook,但是通常(我在Google OAuth2和Azure AD以及Azure AD B2C上都有经验),身份验证提供程序允许您使用custom URI方案进行身份验证回调,类似于badcompany://auth

为了获得身份验证令牌,我最终实现了以下方案(所有代码均提供无担保,请勿随意复制。)

1。启动应用程序时注册URI处理程序

您可以通过在Windows注册表中的HKEY_CURRENT_USER/Software/Classes中创建一个密钥来注册URI处理程序(因此不需要管理特权)>

  • 密钥名称等于URI前缀,在我们的情况下为badcompany
  • 键包含一个名为URL Protocol的空字符串值>
  • 键包含图标的子键DefaultIcon(实际上我不知道这是否必要),我使用了当前可执行文件的路径
  • 有一个子项shell/open/command,其默认值确定试图打开URI时要执行的命令的路径,请注意*,"%1"是将URI传递给可执行文件所必需的。
    this.EnsureKeyExists(Registry.CurrentUser, "Software/Classes/badcompany", "URL:BadCo Applications");
    this.SetValue(Registry.CurrentUser, "Software/Classes/badcompany", "URL Protocol", string.Empty);
    this.EnsureKeyExists(Registry.CurrentUser, "Software/Classes/badcompany/DefaultIcon", $"{location},1");
    this.EnsureKeyExists(Registry.CurrentUser, "Software/Classes/badcompany/shell/open/command", $"\"{location}\" \"%1\"");

// ...

private void SetValue(RegistryKey rootKey, string keys, string valueName, string value)
{
    var key = this.EnsureKeyExists(rootKey, keys);
    key.SetValue(valueName, value);
}

private RegistryKey EnsureKeyExists(RegistryKey rootKey, string keys, string defaultValue = null)
{
    if (rootKey == null)
    {
        throw new Exception("Root key is (null)");
    }

    var currentKey = rootKey;
    foreach (var key in keys.Split('/'))
    {
        currentKey = currentKey.OpenSubKey(key, RegistryKeyPermissionCheck.ReadWriteSubTree) 
                     ?? currentKey.CreateSubKey(key, RegistryKeyPermissionCheck.ReadWriteSubTree);

        if (currentKey == null)
        {
            throw new Exception("Could not get or create key");
        }
    }

    if (defaultValue != null)
    {
        currentKey.SetValue(string.Empty, defaultValue);
    }

    return currentKey;
}

2。打开用于IPC的管道

由于必须将消息从程序的一个实例传递到另一个实例,因此必须打开可用于该目的的命名管道。

我在后台Task中循环调用了此代码

private async Task<string> ReceiveTextFromPipe(CancellationToken cancellationToken)
{
    string receivedText;

    PipeSecurity ps = new PipeSecurity();
    System.Security.Principal.SecurityIdentifier sid = new System.Security.Principal.SecurityIdentifier(System.Security.Principal.WellKnownSidType.WorldSid, null);
    PipeAccessRule par = new PipeAccessRule(sid, PipeAccessRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow);
    ps.AddAccessRule(par);

    using (var pipeStream = new NamedPipeServerStream(this._pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Message, PipeOptions.Asynchronous, 4096, 4096, ps))
    {
        await pipeStream.WaitForConnectionAsync(cancellationToken);

        using (var streamReader = new StreamReader(pipeStream))
        {
            receivedText = await streamReader.ReadToEndAsync();
        }
    }

    return receivedText;
}

3。确保仅启动应用程序once

可以使用Mutex获取。

Mutex

当另一个实例获取了互斥锁时,该应用程序没有完全启动,但是

4。使用身份验证重定向URI调用的句柄
  1. 如果是唯一(第一个)实例,它可能会处理身份验证重定向URI本身
    • 从URI中提取令牌
    • 存储令牌(如果需要和/或需要)

  • 使用令牌进行请求
  • 如果是另外一个实例
    • 使用管道将重定向URI传递给第一个实例
  • 第一个实例现在执行1。
  • ]下的步骤
  • 关闭第二个实例
  • URI通过]发送到第一个实例>

    internal class SingleInstanceChecker
    {
        private static Mutex Mutex { get; set; }
    
        public static async Task EnsureIsSingleInstance(string id, Action onIsSingleInstance, Func<Task> onIsSecondaryInstance)
        {
            SingleInstanceChecker.Mutex = new Mutex(true, id, out var isOnlyInstance);
            if (!isOnlyInstance)
            {
                await onIsSecondaryInstance();
                Application.Current.Shutdown(0);
            }
            else
            {
                onIsSingleInstance();
            }
        }
    }
     

    添加到保罗的出色答案:

    • using (var client = new NamedPipeClientStream(this._pipeName)) { try { var millisecondsTimeout = 2000; await client.ConnectAsync(millisecondsTimeout); } catch (Exception) { onSendFailed(); return; } if (!client.IsConnected) { onSendFailed(); } using (StreamWriter writer = new StreamWriter(client)) { writer.Write(stringToSend); writer.Flush(); } } 值得一看-他们将为您做的一件事是建议对本机应用程序使用授权码流(PKCE)
    • 我的偏好与Paul的偏好相同-使用自定义URI方案-可用性更好,我认为
  • 话虽如此,环回解决方案应该在没有管理员权限的情况下可以用于大于1024的端口
  • 如果有帮助,我的博客上有一些关于此的内容-包括一个Nodejs / Electron示例,您可以从Identity Model Libraries运行以查看完成的解决方案是什么样。


    0
    投票

    添加到保罗的出色答案:

    • using (var client = new NamedPipeClientStream(this._pipeName)) { try { var millisecondsTimeout = 2000; await client.ConnectAsync(millisecondsTimeout); } catch (Exception) { onSendFailed(); return; } if (!client.IsConnected) { onSendFailed(); } using (StreamWriter writer = new StreamWriter(client)) { writer.Write(stringToSend); writer.Flush(); } } 值得一看-他们将为您做的一件事是建议对本机应用程序使用授权码流(PKCE)
    • 我的偏好与Paul的偏好相同-使用自定义URI方案-可用性更好,我认为
    © www.soinside.com 2019 - 2024. All rights reserved.