wpfazuremicrosoft-graph-apiasp.net-core-webapidesktop-application

How to get Users' names list from azure AD app in a WPF application?


I am building a WPF application, and I want to retrieve the names of all users from an Azure AD app when a button is clicked or in a dropdown. I have written an HttpGet method in an ASP.NET Core Web API to achieve this, and I have tested it using Postman (with OAuth 2.0), where I can successfully see the names of all users. However, when I call the HttpGet endpoint from the WPF application, I am encountering errors.

This is my HttpGet endpoint, which returns all users' names from the Azure AD app when tested in Postman (OAuth 2.0 where I have provided client ID, client secret, scope, etc) -

[Route("api/[controller]")]
[ApiController]
public class UsersController : ControllerBase
{
    private readonly ITokenAcquisition _tokenAcquisition;
    private readonly ILogger<UsersController> _logger;

    public UsersController(ITokenAcquisition tokenAcquisition, ILogger<UsersController> logger)
    {
        _tokenAcquisition = tokenAcquisition;
        _logger = logger;
    }

    [HttpGet]
    public async Task<IActionResult> GetUsers()
    {
        try
        {
            var token = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { "User.ReadBasic.All", "User.Read" });
            var graphClient = new GraphServiceClient(new AuthProvider(token));

            var usersList = new List<string>();
            var usersPage = await graphClient.Users
                .Request()
                .Select(u => new { u.DisplayName })
                .GetAsync();

            usersList.AddRange(usersPage.Select(u => u.DisplayName));

            while (usersPage.NextPageRequest != null)
            {
                usersPage = await usersPage.NextPageRequest.GetAsync();
                usersList.AddRange(usersPage.Select(u => u.DisplayName));
            }

            return Ok(usersList);

        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred while fetching users.");
            return StatusCode(StatusCodes.Status500InternalServerError, "An error occurred while fetching users.");
        }
    }

    private class AuthProvider : IAuthenticationProvider
    {
        private readonly string _token;
        public AuthProvider(string token)
        {
            _token = token;
        }
        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
            await Task.CompletedTask;
        }
    }
}

Calling API in my WPF app (I am not sure whether I am using right approach)-

public partial class MainWindow : Window
{
    private static readonly HttpClient client = new HttpClient();
    private static IConfidentialClientApplication confidentialClientApp;

    public MainWindow()
    {
        InitializeComponent();

        // Initialize MSAL ConfidentialClientApplication with client secret
        confidentialClientApp = ConfidentialClientApplicationBuilder.Create("47......")
            .WithClientSecret("gF......")
            .WithAuthority(AzureCloudInstance.AzurePublic, "d81e......")
            .WithRedirectUri("http://localhost")
            .Build();
    }

    private async void FetchDataButton_Click(object sender, RoutedEventArgs e)
    {
        try
        {
            // Acquire token silently
            string[] scopes = new string[] { "api://47....../.default" };
            AuthenticationResult authResult = await confidentialClientApp.AcquireTokenForClient(scopes).ExecuteAsync();

            // Set the token in the request header
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);

            // Make the HTTP GET request
            string apiUrl = "https://localhost:7159/api/Users";
            var response = await client.GetAsync(apiUrl);

            if (response.IsSuccessStatusCode)
            {
                string responseData = await response.Content.ReadAsStringAsync();
                MessageBox.Show(responseData, "API Response", MessageBoxButton.OK, MessageBoxImage.Information);
            }
            else
            {
                MessageBox.Show("Error: " + response.ReasonPhrase, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }
        catch (MsalException msalEx)
        {
            MessageBox.Show("Authentication error: " + msalEx.Message, "Authentication Error", MessageBoxButton.OK, MessageBoxImage.Error);
        }
        catch (Exception ex)
        {
            MessageBox.Show("Exception: " + ex.Message, "Exception", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }
}

But I am getting an error - MsalUiRequiredException: AADSTS50058: A silent sign-in request was sent but no user is signed in.

What can I do to call this HttpGet endpoint & display users names list in my WPF app?


Solution

  • In my case, I registered one application and exposed an API with scope like this:

    enter image description here

    In API permissions tab, I added User.Read.All permission of Application type that works with client credentials flow:

    enter image description here

    Now, I created another application named MyWpfApp and granted exposed API permission as below:

    enter image description here

    Please find below code samples that worked in my case:

    UsersController.cs:

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Graph;
    using Microsoft.Identity.Web;
    using Microsoft.Kiota.Abstractions.Authentication;
    using Microsoft.Kiota.Abstractions;
    
    [Route("api/[controller]")]
    [ApiController]
    public class UsersController : ControllerBase
    {
        private readonly ITokenAcquisition _tokenAcquisition;
    
        public UsersController(ITokenAcquisition tokenAcquisition)
        {
            _tokenAcquisition = tokenAcquisition;
        }
    
        [HttpGet]
        public async Task<IActionResult> GetUsers()
        {
            try
            {
                var token = await _tokenAcquisition.GetAccessTokenForAppAsync("https://graph.microsoft.com/.default");
                var graphClient = new GraphServiceClient(new AuthProvider(token));
    
                // Fetch users with displayName
                var users = await graphClient.Users.GetAsync((requestConfiguration) =>
                {
                    requestConfiguration.QueryParameters.Select = new string[] { "displayName" };
                });
    
                if (users == null || users.Value == null)
                {
                    return StatusCode(500, "Error retrieving users: Users response is null");
                }
    
                var usersList = users.Value
                    .Where(u => u.DisplayName != null)
                    .Select(u => u.DisplayName)
                    .ToList();
    
                return Ok(usersList);
            }
            catch (System.Exception ex)
            {
                return StatusCode(500, $"Error retrieving users: {ex.Message}");
            }
        }
    
        private class AuthProvider : IAuthenticationProvider
        {
            private readonly string _token;
    
            public AuthProvider(string token)
            {
                _token = token;
            }
    
            public Task AuthenticateRequestAsync(RequestInformation request, Dictionary<string, object>? additionalAuthenticationContext = null, CancellationToken cancellationToken = default)
            {
                request.Headers.Add("Authorization", $"Bearer {_token}");
                return Task.CompletedTask;
            }
        }
    }
    

    When I ran the above API, I'm able to retrieve the users successfully by calling below endpoint:

    GET https://localhost:7261/api/Users
    

    Response:

    enter image description here

    To get the same response in WPF app when button is clicked, you can make use of below sample code files:

    MainWindow.xaml:

    <Window x:Class="MyWpfApp.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="My WPF App" Height="350" Width="525">
        <Grid>
            <Button Name="FetchDataButton" Content="Fetch Users" Click="FetchDataButton_Click" Width="150" Height="40" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10,10,0,0" />
            <ListBox Name="UsersListBox" Width="400" Height="200" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10,60,0,0" />
        </Grid>
    </Window>
    

    MainWindow.xaml.cs:

    using Microsoft.Identity.Client;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Net.Http.Json;
    using System.Windows;
    
    namespace MyWpfApp
    {
        public partial class MainWindow : Window
        {
            private static readonly HttpClient client = new HttpClient();
            private static IConfidentialClientApplication confidentialClientApp;  
    
            public MainWindow()
            {
                InitializeComponent();
                
                confidentialClientApp = ConfidentialClientApplicationBuilder.Create("wpfappId")
                    .WithClientSecret("wpfappsecret")
                    .WithAuthority(new Uri("https://login.microsoftonline.com/tennatId"))
                    .Build();
            }
    
            private async void FetchDataButton_Click(object sender, RoutedEventArgs e)
            {
                try
                {
                    string[] scopes = { "api://myuserapiappId/.default" };
                    AuthenticationResult authResult = await confidentialClientApp.AcquireTokenForClient(scopes).ExecuteAsync();
    
                    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
    
                    string apiUrl = "https://localhost:7261/api/Users";
                    var users = await client.GetFromJsonAsync<string[]>(apiUrl);
    
                    UsersListBox.Items.Clear();
                    if (users != null)
                    {
                        foreach (var user in users)
                        {
                            UsersListBox.Items.Add(user);
                        }
                    }
                    else
                    {
                        MessageBox.Show("No users found.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
                    }
                }
                catch (MsalException msalEx)
                {
                    MessageBox.Show("Authentication error: " + msalEx.Message, "Authentication Error", MessageBoxButton.OK, MessageBoxImage.Error);
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Error: " + ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
                }
            }
        }
    }
    

    Response:

    enter image description here