Overview: I would like to create a solution for an organization using MSAL. The company seems to have users under a single Azure AD tenant. My solution should use a Blazor WASM UI with a .NET API running server side. I want to authenticate the users for the organization in the UI and pass the token to the API where it will call Microsoft Graph(for email data mostly).
Question: What should my Azure Tenant setup look like? Entra Id or Azure AD B2C?
Attempted methods and further information: I have setup (and somewhat got working) a regular Entra ID tenant. I have also tried Azure AD B2C, but ran into the issue where the API couldn't delegate permissions to the Graph API. Ideally this will be somewhat easy to package and serve to the organization for them to manage.
I agree with @Peter Bons, it is best to use existing Microsoft Entra ID tenant rather than opting for Azure AD B2C.
In my case, I referred this GitHub sample for enabling Blazor WASM UI to sign in users and call Microsoft Graph API. Initially, I registered one application and granted Mail.Read
permission of Delegated type in it with consent as below:
In Authentication
tab, I added https://localhost:44314/authentication/login-callback as redirect URI under SPA platform, along with logout URL:
After cloning the GitHub sample, I opened WebApp-graph-user/Call-MSGraph/blazorwasm-calls-MS-graph path and updated below code files:
UserProfileBase.cs:
using Microsoft.AspNetCore.Components;
using Microsoft.Graph;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace blazorwasm_calls_MS_graph.Pages
{
public class UserProfileBase : ComponentBase
{
[Inject]
GraphServiceClient GraphClient { get; set; }
protected User _user = new User();
protected List<Message> _messages = new List<Message>();
protected string ErrorMessage { get; set; }
protected override async Task OnInitializedAsync()
{
await GetUserProfile();
await GetUserMessages();
}
private async Task GetUserProfile()
{
try
{
var request = GraphClient.Me.Request();
_user = await request.GetAsync();
Console.WriteLine($"User Profile fetched: {_user.DisplayName}");
}
catch (Exception ex)
{
ErrorMessage = $"Error fetching user profile: {ex.Message}";
Console.WriteLine(ErrorMessage);
}
}
private async Task GetUserMessages()
{
try
{
var request = GraphClient.Me.Messages.Request();
var messages = await request.Top(10).GetAsync(); // Fetch top 10 email messages
if (messages == null || !messages.CurrentPage.Any())
{
ErrorMessage = "No email messages found.";
Console.WriteLine("No email messages found.");
}
else
{
_messages = messages.CurrentPage.ToList();
Console.WriteLine($"Fetched {_messages.Count} email messages.");
}
}
catch (Exception ex)
{
ErrorMessage = $"Error fetching email messages: {ex.Message}";
Console.WriteLine(ErrorMessage);
}
}
}
}
UserProfile.Razor:
@page "/profile"
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
@inherits UserProfileBase
<h3>User Profile</h3>
@if (!string.IsNullOrEmpty(ErrorMessage))
{
<div class="alert alert-danger">
<strong>Error:</strong> @ErrorMessage
</div>
}
else
{
<table class="table">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tr>
<td>DisplayName</td>
<td>@_user.DisplayName</td>
</tr>
<tr>
<td>UserPrincipalName</td>
<td>@_user.UserPrincipalName</td>
</tr>
</table>
}
<h3>Email Messages</h3>
@if (_messages == null || !_messages.Any())
{
<p>No email messages found.</p>
}
else
{
<ul>
@foreach (var message in _messages)
{
@if (message != null && message.From != null && message.From.EmailAddress != null)
{
<li>
<strong>@message.Subject</strong><br />
From: @message.From.EmailAddress.Address<br />
Sent: @message.SentDateTime?.ToString("g")<br />
Body Preview: @message.BodyPreview
</li>
}
else
{
<li>Invalid message data.</li>
}
}
</ul>
}
When I ran the project now and visited https://localhost:44314 in browser, it asked me to sign in like this:
After successful login, it showed signed-in user's email data in Profile page as below: