Search code examples
asp.net-coreazure-web-app-serviceasp.net-core-webapiblazor-server-sideblazor-webassembly

Blazor Server App Get All Users Using Graph Api


I am currently logging into my application using Microsoft login; a client wants to have list of users in my organisation in a dropdown, I was provided with delegated User.Read.All permission to get the details, but I tried with several methods but none of help me, or I think I don't know the right way to do it.

This is my login page:

@page "/"
@page "/login"
@layout LoginLayout
@using Azure.Core;
@using blazorServer.Shared.Models
@using System;
@using System.Text.Json;
@using System.Net.Http.Json
@using System.Collections.Generic;
@using System.Collections.ObjectModel;
@using BlazorServer.UI.Helper;
@using Microsoft.Extensions.Configuration;
@using System.Security.Claims
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.WebUtilities;
@using Microsoft.Extensions.Primitives;
@using Microsoft.Graph;
@using Microsoft.Graph.Core;
@using Azure.Identity;
@using Microsoft.Identity.Web;
@using System.Net.Http.Headers;
@using Microsoft.Identity.Client;
@using System.IdentityModel.Tokens.Jwt;
@using System.Threading;

@inject HttpClient Http
@inject NavigationManager NavigationManager
@inject IConfiguration configuration
@inject IHttpClientFactory ClientFactory
@inject Blazored.SessionStorage.ISessionStorageService sessionStorage
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject IStorage LocalStorage
@inject AuthenticationStateProvider AuthenticationStateProvider


<Spin Spinning=_loading Tip="Processing...">
<GridRow Align="center" Style="minHeight:100vh;height:100vh;overflow:
">
    <GridCol Span="18">
        <Image Preview=false Src="/loginscreen.jpg" Height="100%" Width="100%" />
    </GridCol>
    <GridCol Xs="6" Style="background-color:#004a87;">
        <GridRow Align="center">
            <GridCol Span="24" Style="text-align:center;">
                <Image Preview=false Src="/dd.svg" Alt="logo" Height="150px" Width="150px" />
                <br />
                <Button Loading="true">
                    <span style="color:black;font-size:larger">user authenticating...</span>
                </Button>
            </GridCol>
           
        </GridRow>
        <br />
        <br />
        
        <br />
    </GridCol>
</GridRow>
</Spin>

@code {
    string style = "background: #0092ff; padding: 0 0 0 0;";

    public class Model
    {
        public string Username { get; set; }
        public string Password { get; set; }
        public bool WindowsLogin { get; set; } = true;
    }

    private Model model = new Model();
    AntDesign.Form<Model> form;
    private bool auto = false;
    private string apiURL;
    private UserSession userSession;
    private bool _loading = false;
    private string returnUrl;

    protected override void OnInitialized()
    {
        apiURL = configuration.GetValue<string>("WebApiUrl");
        var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
        StringValues values;

        if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("returnUrl", out values))
        {
            returnUrl = Convert.ToString(values);
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        try
        {
            _loading = true;
            var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
            var user = authState.User;

            if (user.Identity.IsAuthenticated)
            {
                var data = await GetAccessToken();
                var userName = user.Identity.Name;
                var userEmail = user.FindFirst(ClaimTypes.Email)?.Value;
                var userCredentials = await Http.GetFromJsonAsync<List<UserModel>>(apiURL + "api/admin/getuserbyemail?email=" + user.Identity.Name);

                if (userCredentials != null && userCredentials.Count > 0)
                {
                    userSession = new()
                        {
                            Name = userCredentials.First().FirstName + " " + userCredentials.First().LastName,
                            Email = userCredentials.First().Email,
                            UserId = userCredentials.First().FirstName + " " + userCredentials.First().LastName,
                            FirstName = userCredentials.First().FirstName,
                            UserRoles = userCredentials.Select(x => x.Role).ToList()

                        };

                    if (userSession.UserId == null || userSession.UserId.Length <= 0)
                    {
                        userSession.UserId = userSession.Name.Contains("\\") ? userSession.Name.Split("\\")[1] : userSession.Name;
                    }
                    await sessionStorage.SetItemAsync("UserSession", userSession);
                    if (string.IsNullOrWhiteSpace(returnUrl))
                    {
                        NavigationManager.NavigateTo("/WorkList");
                    }
                    else
                    {
                        NavigationManager.NavigateTo($"/{returnUrl}", true);
                    }
                }
                else
                {
                    userSession = new()
                        {
                            Name = user.Identity.Name,
                            Email = user.FindFirst(c => c.Type == ClaimTypes.Email)?.Value,
                            UserId = user.FindFirst(c => c.Type == ClaimTypes.WindowsAccountName)?.Value,
                            FirstName = user.FindFirst(c => c.Type == ClaimTypes.Surname)?.Value
                        };

                    if (userSession.UserId == null || userSession.UserId.Length <= 0)
                    {
                        userSession.UserId = userSession.Name.Contains("\\") ? userSession.Name.Split("\\")[1] : userSession.Name;
                    }

                    await sessionStorage.SetItemAsync("UserSession", userSession);
                    if (string.IsNullOrWhiteSpace(returnUrl))
                    {
                        NavigationManager.NavigateTo("/dd");
                    }
                    else
                    {
                        NavigationManager.NavigateTo($"/{returnUrl}", true);
                    }
                    //await DisplayNotification(NotificationType.Error, "Error", "The user is NOT authenticated");
                }
            }
            else
            {
                await DisplayNotification(NotificationType.Error, "Error", "The user is NOT authenticated");
            }

            _loading = false;
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex);
            throw ex;
        }
    }

    private async Task<string> GetAccessToken()
    {
        try
        {
            var tenantId = "02ddddddb";
            var clientId = "bd4a11ffddddsfsdfasf428";
            var clientSecret = "sdfasfasfa";

            var cca22 = ConfidentialClientApplicationBuilder    
             .Create(clientId)
             .WithClientSecret(clientSecret)
             .Build();

            var scopes = new[] { "Application.Read.All" };

            var result22 = await cca22.AcquireTokenForClient(scopes)
             .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
             .ExecuteAsync();

            string accessToken22 = result22.AccessToken;

            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken22);

                var response = await client.GetAsync("https://graph.microsoft.com/v1.0/users");

                if (response.IsSuccessStatusCode)
                {
                    var jsonResponse = await response.Content.ReadAsStringAsync();
                    // Process jsonResponse containing user details
                }
            }
         
            return result.AccessToken;
        }
        catch(Exception e)
        {
            Console.WriteLine(e);
            return e.Message;
        }   
    }
}

Here's my program.cs file:

using Microsoft.Identity.Web.UI;
using Microsoft.Identity.Web;
using blazor.UI.Helper;
using AntDesign;
using Blazored.SessionStorage;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;

namespace blazorserver.UI
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ') ?? 
                                builder.Configuration["MicrosoftGraph:Scope"]?.Split(' ');

            // Add services to the container.
            builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

            builder.Services.AddControllersWithViews()
               .AddMicrosoftIdentityUI();

            builder.Services.AddServerSideBlazor()
                .AddMicrosoftIdentityConsentHandler();

            //Add Local and Session Storage
            builder.Services.AddRazorPages();
            builder.Services.AddServerSideBlazor();
            builder.Services.AddHttpClient();
            builder.Services.AddAntDesign();
            builder.Services.AddBlazoredSessionStorage();
            builder.Services.AddHttpContextAccessor();
            builder.Services.AddScoped<NotificationService>();
            builder.Services.AddScoped<IStorage, LocalStorage>();

            //Authentication

            // Copied from windows auth project
            // Add services to the container.
            //builder.Services.AddAuthentication(NegotiateDefaults.AuthenticationScheme)
               //.AddNegotiate();

            // Copied from windows auth project
            builder.Services.AddAuthorization(options =>
            {
                // By default, all incoming requests will be authorized according to the default policy
                options.FallbackPolicy = options.DefaultPolicy;
            });

            var app = builder.Build();

            app.UseCookiePolicy(new CookiePolicyOptions
            {
                Secure = CookieSecurePolicy.Always
            });

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();

            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.MapControllers();
            app.MapBlazorHub();
            app.MapFallbackToPage("/_Host");

            app.Run();
        }
    }
}

I am expecting to get all users in my organisation and save it in session so I can populate the dropdown


Solution

  • Firstly, you have builder.Services.AddServerSideBlazor(); and app.MapBlazorHub(); so that you are using blazor server and not the latest blazor web app in .net 8 I think. Then there are 2 methods for you to get the user lists via MS Graph API. One for using client credential flow to generate access token with Application API permission like what you tried in GetAccessToken(). Another is using auth code flow to generate access token with delegated API permission.

    Let's see the API document, both Application type or delegated type require Directory.Read.All permission to read all users. So if you haven't assigned this API permission, please add it.

    For using delegated API permission, we have official sample here to call Graph Api. I had a test with a new Blazor server application created by VS template, and I also choose the authentication type as Microsoft identity platform so that the new project would contain Azure AD by default. Then I add packages below.

    <PackageReference Include="Microsoft.Graph" Version="4.43.0" />
    <PackageReference Include="Microsoft.Identity.Client" Version="4.52.0" />
    <PackageReference Include="Microsoft.Identity.Web" Version="1.26.0" />
    <PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.26.0" />
    <PackageReference Include="Microsoft.Identity.Web.UI" Version="1.26.0" />
    

    Add Graph SDK into the app.

    builder.Services
           .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
           .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
           .EnableTokenAcquisitionToCallDownstreamApi()
           .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
           .AddInMemoryTokenCaches();
    

    Then inject the MS Graph SDK into FetchData.razor:

    @using Microsoft.Graph
    
    [Inject]
    GraphServiceClient GraphClient { get; set; }
    
    protected override async Task OnInitializedAsync()
    {
        var users = await GraphClient.Users.Request().GetAsync();
        forecasts = await ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
    }
    

    enter image description here

    For using Application API permission. That's easy, just using code below.

    @using Azure.Identity
    @using Microsoft.Graph
    
    var scopes = new[] { "https://graph.microsoft.com/.default" };
    var tenantId = "xxx";
    var clientId = "xxxx";
    var clientSecret = "xxxx";
    
    var clientSecretCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
    var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
    
    var res = await graphClient.Users.Request().GetAsync();
    

    enter image description here