使用 HttpListener 的 windows 桌面应用程序的 OAuth 2.0 授权
OAuth 2.0 Authorization for windows desktop application using HttpListener
我正在用 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);
}
}
我不知道 Facebook,但通常(我熟悉 Google OAuth2 和 Azure AD 以及 Azure AD B2C),身份验证提供程序允许您使用 自定义 身份验证回调的 URI 方案,类似于 badcompany://auth
为了获得身份验证令牌,我最终实施了以下方案(所有代码均在没有保证的情况下提供,不得随意复制。)
1。在应用程序启动时注册一个 URI 处理程序
您可以通过在 Windows 注册表
的 HKEY_CURRENT_USER/Software/Classes
键(因此不需要管理员权限)中创建键来注册 URI 处理程序
- 密钥的名称等于 URI 前缀,
badcompany
在我们的例子中
- 密钥包含一个名为
URL Protocol
的空字符串值
- key包含一个子key
DefaultIcon
作为icon(其实我也不知道有没有这个必要),我用的是当前可执行文件的路径
- 有一个子键
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
调用的句柄
- 如果它是唯一的(第一个)实例,它可能会自己处理身份验证重定向 URI
- 从 URI 中提取令牌
- 存储令牌(如果需要 and/or 需要)
- 为请求使用令牌
- 如果是进一步的实例
- 使用管道将重定向 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();
}
}
添加到 Paul 的出色回答中:
- Identity Model Libraries 值得一看 - 他们将为您做的事情之一是授权代码流 (PKCE),推荐用于本机应用程序
- 我的偏好与 Paul 的相同 - 使用自定义 URI 方案 - 我认为可用性更好
- 话虽如此,对于大于 1024 的端口,环回解决方案应该在没有管理员权限的情况下工作
如果有帮助,我的博客上有一些关于此的内容 - 包括 Nodejs / Electron 示例,您可以 运行 来自 here 以查看完成的解决方案。
我正在用 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);
}
}
我不知道 Facebook,但通常(我熟悉 Google OAuth2 和 Azure AD 以及 Azure AD B2C),身份验证提供程序允许您使用 自定义 身份验证回调的 URI 方案,类似于 badcompany://auth
为了获得身份验证令牌,我最终实施了以下方案(所有代码均在没有保证的情况下提供,不得随意复制。)
1。在应用程序启动时注册一个 URI 处理程序
您可以通过在 Windows 注册表
的HKEY_CURRENT_USER/Software/Classes
键(因此不需要管理员权限)中创建键来注册 URI 处理程序
- 密钥的名称等于 URI 前缀,
badcompany
在我们的例子中 - 密钥包含一个名为
URL Protocol
的空字符串值
- key包含一个子key
DefaultIcon
作为icon(其实我也不知道有没有这个必要),我用的是当前可执行文件的路径 - 有一个子键
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
调用的句柄- 如果它是唯一的(第一个)实例,它可能会自己处理身份验证重定向 URI
- 从 URI 中提取令牌
- 存储令牌(如果需要 and/or 需要)
- 为请求使用令牌
- 如果是进一步的实例
- 使用管道将重定向 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();
}
}
添加到 Paul 的出色回答中:
- Identity Model Libraries 值得一看 - 他们将为您做的事情之一是授权代码流 (PKCE),推荐用于本机应用程序
- 我的偏好与 Paul 的相同 - 使用自定义 URI 方案 - 我认为可用性更好
- 话虽如此,对于大于 1024 的端口,环回解决方案应该在没有管理员权限的情况下工作
如果有帮助,我的博客上有一些关于此的内容 - 包括 Nodejs / Electron 示例,您可以 运行 来自 here 以查看完成的解决方案。