【问题标题】:OAuth 2.0 Authorization for windows desktop application using HttpListener使用 HttpListener 的 Windows 桌面应用程序的 OAuth 2.0 授权
【发布时间】:2020-06-18 02:21:11
【问题描述】:

我正在用 C# 编写一个带有外部身份验证(Google、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


    【解决方案1】:

    补充保罗的出色回答:

    • Identity Model Libraries 值得一看 - 他们会为您做的一件事是授权代码流 (PKCE),推荐用于本机应用
    • 我的偏好与 Paul 相同 - 使用自定义 URI 方案 - 我认为可用性更好
    • 话虽如此,对于大于 1024 的端口,环回解决方案应该在没有管理员权限的情况下工作

    如果有帮助,我的博客上有一些关于此的内容 - 包括 Nodejs / Electron 示例,您可以从 here 运行以查看完成的解决方案是什么样的。

    【讨论】:

      【解决方案2】:

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

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

      1。在应用启动时注册一个 URI-handler

      您可以通过在 Windows 注册表中的 HKEY_CURRENT_USER/Software/Classes(因此不需要管理员权限)键中创建一个键来注册 URI-Handler

      • 键的名称等于 URI 前缀,在我们的例子中为 badcompany
      • 该键包含一个名为URL Protocol 的空字符串值
      • key中包含了图标的子键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。确保应用程序仅启动一次

      这可以使用Mutex 获得。

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

      当互斥体已被另一个实例获取时,应用程序并没有完全启动,但是

      4。使用身份验证重定向 URI 调用句柄

      1. 如果它是唯一的(第一个)实例,它可以自己处理身份验证重定向 URI
        • 从 URI 中提取令牌
        • 存储令牌(如有必要和/或需要)
        • 将令牌用于请求
      2. 如果是另一个实例
        • 使用管道将重定向 URI 传递给第一个实例
        • 第一个实例现在执行 1 下的步骤。
        • 关闭第二个实例

      URI 被发送到第一个实例

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

      【讨论】:

        猜你喜欢
        • 2023-03-22
        • 1970-01-01
        • 2017-07-23
        • 2021-08-12
        • 2011-04-14
        • 2012-11-25
        • 1970-01-01
        • 2020-11-27
        • 2011-07-15
        相关资源
        最近更新 更多