从 Windows 桌面应用程序调用具有 AD 授权的 Azure 函数
Call Azure Function with AD Authorization from a Windows desktop app
目标:
我想要一个可以通过 Windows 桌面应用调用的 Azure Functions (HttpTrigger)。我希望对该功能的访问由 Active Directory 控制,并且只有授权用户才能调用它。
当前状态:
我按照指南 here 创建了一个具有 AD 授权的桌面应用程序。我还创建了一个 Azure 函数,我向其中添加了一个 "App Service Authentication" 和 "Log in with Azure Active Directory",并创建了一个新的应用程序注册来处理这个问题。在我的桌面应用程序中,我添加了一个调用此函数的按钮。
问题:
当我在浏览器中直接通过 link 调用该函数时,一切正常;如果我获得授权,它会调用该函数,如果我没有获得授权,我将被重定向到登录屏幕,并且在成功登录后(仅限授权用户)我会得到该函数的结果。
当我尝试通过我的桌面应用程序执行此操作时,问题就来了。当我按下函数调用按钮时,我被重定向到登录屏幕,一旦我使用我的凭据成功登录,我就会收到错误消息:
AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application: <app-id>
在我的应用程序注册中,我没有 "Mobile and desktop applications" 的身份验证选项,只有 "Web",就会发生这种情况。
如果我添加 "Mobile and desktop applications" 选项,那么原始按钮(来自上面的教程)可以登录并正常工作(在以前的情况下,它给了我同样的错误)但是这次,当我尝试调用通过我添加的按钮的功能,程序因错误而崩溃:
Inner Exception 1:
HttpRequestException: An error occurred while sending the request.
Inner Exception 2:
WebException: The underlying connection was closed: An unexpected error occurred on a send.
Inner Exception 3:
IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.
Inner Exception 4:
SocketException: An existing connection was forcibly closed by the remote host
如果我强制使用 TLS 1.2,我会收到 401 错误:"You do not have permission to view this directory or page."。如果我尝试调用一个不使用AD授权的函数,那么整个过程就成功了。我的代码:
private async void CallFunctionButton_Click(object sender, RoutedEventArgs e)
{
AuthenticationResult authResult = null;
var app = App.PublicClientApp;
ResultText.Text = string.Empty;
TokenInfoText.Text = string.Empty;
var accounts = await app.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
try
{
authResult = await app.AcquireTokenSilent(scopes, firstAccount)
.ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
try
{
authResult = await app.AcquireTokenInteractive(scopes)
.WithAccount(accounts.FirstOrDefault())
.WithParentActivityOrWindow(new WindowInteropHelper(this).Handle)
.WithPrompt(Prompt.SelectAccount)
.ExecuteAsync();
}
catch (MsalException msalex)
{
ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
}
}
catch (Exception ex)
{
ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}";
return;
}
if (authResult != null)
{
this.SignOutButton.Visibility = Visibility.Visible;
string token = authResult.AccessToken;
using (var client = new HttpClient())
{
// With an explicit selection of the security protocol the program does not crash.
// Instead it gives 401 Unauthorized error, when already signed in.
// Without the following line, the program crashes.
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
string requestUrl = $"the_URL_of_my_function";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = client.SendAsync(request).Result;
var responseString = response.Content.ReadAsStringAsync().Result;
ResultText.Text = responseString;
DisplayBasicTokenInfo(authResult);
}
}
}
问题:
我可以 call/use 需要通过 Windows 桌面应用程序授权的 Azure Functions 吗?
关于该问题,可能与TLS版本有关。据我所知,目前,Azure App Service will be created with TLS 1.2 by default. But, WPF application uses TLS 1.0 by default. So we cannot call the Azure function. Regarding how to fix it, please refer to the document
更新
关于如何调用Azure AD投射的Azure函数,请参考以下步骤
为 Azure 函数配置 Azure AD
在 Azure AD 中创建客户端应用程序
配置API权限并获取我们需要的范围
代码
string[] scopes = new string[]
{"https://testfun08.azurewebsites.net/user_impersonation" };// the scope you copy
private async void CallFunctionButton_Click(object sender, RoutedEventArgs e)
{
// get token
AuthenticationResult authResult = null;
var app = App.PublicClientApp;
ResultText.Text = string.Empty;
TokenInfoText.Text = string.Empty;
var accounts = await app.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
try
{
authResult = await app.AcquireTokenSilent(scopes, firstAccount)
.ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
try
{
authResult = await app.AcquireTokenInteractive(scopes)
.WithAccount(accounts.FirstOrDefault())
.WithParentActivityOrWindow(new WindowInteropHelper(this).Handle)
.WithPrompt(Prompt.SelectAccount)
.ExecuteAsync();
}
catch (MsalException msalex)
{
ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
}
}
catch (Exception ex)
{
ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}";
return;
}
//call Azure function
if (authResult != null)
{
this.SignOutButton.Visibility = Visibility.Visible;
string token = authResult.AccessToken;
using (var client = new HttpClient())
{
// Without the following line, the program crashes.
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
string requestUrl = $"the_URL_of_my_function";
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
HttpResponseMessage response = client.SendAsync(request).Result;
var responseString = response.Content.ReadAsStringAsync().Result;
ResultText.Text = responseString;
DisplayBasicTokenInfo(authResult);
}
}
}
目标: 我想要一个可以通过 Windows 桌面应用调用的 Azure Functions (HttpTrigger)。我希望对该功能的访问由 Active Directory 控制,并且只有授权用户才能调用它。
当前状态: 我按照指南 here 创建了一个具有 AD 授权的桌面应用程序。我还创建了一个 Azure 函数,我向其中添加了一个 "App Service Authentication" 和 "Log in with Azure Active Directory",并创建了一个新的应用程序注册来处理这个问题。在我的桌面应用程序中,我添加了一个调用此函数的按钮。
问题: 当我在浏览器中直接通过 link 调用该函数时,一切正常;如果我获得授权,它会调用该函数,如果我没有获得授权,我将被重定向到登录屏幕,并且在成功登录后(仅限授权用户)我会得到该函数的结果。 当我尝试通过我的桌面应用程序执行此操作时,问题就来了。当我按下函数调用按钮时,我被重定向到登录屏幕,一旦我使用我的凭据成功登录,我就会收到错误消息:
AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application: <app-id>
在我的应用程序注册中,我没有 "Mobile and desktop applications" 的身份验证选项,只有 "Web",就会发生这种情况。 如果我添加 "Mobile and desktop applications" 选项,那么原始按钮(来自上面的教程)可以登录并正常工作(在以前的情况下,它给了我同样的错误)但是这次,当我尝试调用通过我添加的按钮的功能,程序因错误而崩溃:
Inner Exception 1:
HttpRequestException: An error occurred while sending the request.
Inner Exception 2:
WebException: The underlying connection was closed: An unexpected error occurred on a send.
Inner Exception 3:
IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.
Inner Exception 4:
SocketException: An existing connection was forcibly closed by the remote host
如果我强制使用 TLS 1.2,我会收到 401 错误:"You do not have permission to view this directory or page."。如果我尝试调用一个不使用AD授权的函数,那么整个过程就成功了。我的代码:
private async void CallFunctionButton_Click(object sender, RoutedEventArgs e)
{
AuthenticationResult authResult = null;
var app = App.PublicClientApp;
ResultText.Text = string.Empty;
TokenInfoText.Text = string.Empty;
var accounts = await app.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
try
{
authResult = await app.AcquireTokenSilent(scopes, firstAccount)
.ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
try
{
authResult = await app.AcquireTokenInteractive(scopes)
.WithAccount(accounts.FirstOrDefault())
.WithParentActivityOrWindow(new WindowInteropHelper(this).Handle)
.WithPrompt(Prompt.SelectAccount)
.ExecuteAsync();
}
catch (MsalException msalex)
{
ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
}
}
catch (Exception ex)
{
ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}";
return;
}
if (authResult != null)
{
this.SignOutButton.Visibility = Visibility.Visible;
string token = authResult.AccessToken;
using (var client = new HttpClient())
{
// With an explicit selection of the security protocol the program does not crash.
// Instead it gives 401 Unauthorized error, when already signed in.
// Without the following line, the program crashes.
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
string requestUrl = $"the_URL_of_my_function";
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = client.SendAsync(request).Result;
var responseString = response.Content.ReadAsStringAsync().Result;
ResultText.Text = responseString;
DisplayBasicTokenInfo(authResult);
}
}
}
问题: 我可以 call/use 需要通过 Windows 桌面应用程序授权的 Azure Functions 吗?
关于该问题,可能与TLS版本有关。据我所知,目前,Azure App Service will be created with TLS 1.2 by default. But, WPF application uses TLS 1.0 by default. So we cannot call the Azure function. Regarding how to fix it, please refer to the document
更新
关于如何调用Azure AD投射的Azure函数,请参考以下步骤
为 Azure 函数配置 Azure AD
在 Azure AD 中创建客户端应用程序
配置API权限并获取我们需要的范围
代码
string[] scopes = new string[]
{"https://testfun08.azurewebsites.net/user_impersonation" };// the scope you copy
private async void CallFunctionButton_Click(object sender, RoutedEventArgs e)
{
// get token
AuthenticationResult authResult = null;
var app = App.PublicClientApp;
ResultText.Text = string.Empty;
TokenInfoText.Text = string.Empty;
var accounts = await app.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
try
{
authResult = await app.AcquireTokenSilent(scopes, firstAccount)
.ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
try
{
authResult = await app.AcquireTokenInteractive(scopes)
.WithAccount(accounts.FirstOrDefault())
.WithParentActivityOrWindow(new WindowInteropHelper(this).Handle)
.WithPrompt(Prompt.SelectAccount)
.ExecuteAsync();
}
catch (MsalException msalex)
{
ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
}
}
catch (Exception ex)
{
ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}";
return;
}
//call Azure function
if (authResult != null)
{
this.SignOutButton.Visibility = Visibility.Visible;
string token = authResult.AccessToken;
using (var client = new HttpClient())
{
// Without the following line, the program crashes.
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
string requestUrl = $"the_URL_of_my_function";
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
HttpResponseMessage response = client.SendAsync(request).Result;
var responseString = response.Content.ReadAsStringAsync().Result;
ResultText.Text = responseString;
DisplayBasicTokenInfo(authResult);
}
}
}