Azure Static Web Apps (ASWA) offers a straightforward authentication feature. With this feature, you don't need to write a complicating authentication logic by your hand and can sign in to ASWA. By the way, the authentication details from there only show whether you've logged in or not. If you need more information, you should do something more on your end. Throughout this post, I'm going to discuss how to access your user profile data stored in Azure Active Directory (AAD) through Microsoft Graph from the Blazor WebAssembly (WASM) app running on an ASWA instance.
You can find the sample code used in this post on this GitHub repository (docs in Korean).
Retrieving Authentication Data from Azure Static Web Apps
After publishing your Blazor WASM app to ASWA, the page before log-in might look like this:
If you want to use AAD as your primary identity provider, add the link to the Login
HTML element.
https://<azure_static_webapp>.azurestaticapps.net/.auth/login/aad |
After the sign-in, you can retrieve your authentication details by calling the API endpoint like below. For brevity, I omitted unnecessary codes.
var baseUri = "https://<azure_static_webapp>.azurestaticapps.net"; | |
var http = new HttpClient() { BaseAddress = new Uri(baseUri) }; | |
var response = await http.GetStringAsync("/.auth/me").ConfigureAwait(false); |
Here are the authentication details from the response:
{ | |
"clientPrincipal": { | |
"identityProvider":"aad", | |
"userId":"<guid>", | |
"userDetails":"<logged_in_email>", | |
"userRoles":[ | |
"anonymous", | |
"authenticated" | |
] | |
} | |
} |
As mentioned above, there's only limited information available from the response. Therefore, if you need more user details, you should do some additional work on your end.
Accessing User Data through Microsoft Graph
You only know your email address used for log-in. Here are the facts about your logged-in details:
- You signed in through your tenant where your email belongs.
- The sign-in information TELLS your email address used for log-in.
- The sign-in information DOESN'T TELL the tenant information where you logged in.
- The sign-in information DOESN'T TELL the tenant information where the ASWA is hosted.
- The sign-in information DOESN'T TELL the tenant information where you want to access.
In other words, there are chances that all three tenants details – the tenant where you logged in, the tenant hosting the ASWA instance, and the tenant where you want to access – might be different from each other. All you know of my details are:
- You logged into a tenant, and
- You only know my email address used for log-in.
Then, how can you know your user details from the tenant that you want to access?
First of all, you need to get permission to get the details to the tenant. Although you signed in to ASWA, it doesn't mean you have enough permission to access the resources. Because ASWA offers Azure Functions as its facade API, let's use this feature.
When calling the facade API from the Blazor WASM app side, it always includes the auth details through the request header of x-ms-client-principal
. The information is the Base64 encoded string, which looks like this:
ewogICJpZGVudGl0eVByb3ZpZGVyIjoiYWFkIiwKICAidXNlcklkIjoiPGd1aWQ+IiwKICAidXNlckRldGFpbHMiOiI8bG9nZ2VkX2luX2VtYWlsPiIsCiAgInVzZXJSb2xlcyI6WwogICAgImFub255bW91cyIsCiAgICAiYXV0aGVudGljYXRlZCIKICBdCn0= |
Therefore, decode the string and deserialise it to get the email address for log-in. Here's a POCO class for deserialisation.
public class ClientPrincipal | |
{ | |
[JsonProperty("identityProvider")] | |
public string IdentityProvider { get; set; } | |
[JsonProperty("userId")] | |
public string UserId { get; set; } | |
[JsonProperty("userDetails")] | |
public string UserDetails { get; set; } | |
[JsonProperty("userRoles")] | |
public IEnumerable<string> UserRoles { get; set; } | |
} |
With this POCO class, deserialise the header value and get the email address you're going to utilise.
var bytes = Convert.FromBase64String((string)req.Headers["x-ms-client-principal"]); | |
var json = Encoding.UTF8.GetString(bytes); | |
var principal = JsonConvert.DeserializeObject<ClientPrincipal>(json); | |
var userEmail = principal.UserDetails; |
All the plumbing to get the user details is done. Let's move on.
Registering App on Azure Active Directory
The next step is to register an app on AAD through Azure Portal. I'm not going to go further for this step but will give you this document to get it done. Once you complete app registration, you should give it appropriate roles and permissions, which is the application permission instead of the delegate permission. For example, User.Read.All
permission should be enough for this exercise.
Once you complete this step, you'll have TenantID
, ClientID
and ClientSecret
information.
Microsoft Authentication Library (MSAL) for .NET
You first need to get an access token to retrieve your details stored on AAD. There are many ways to get the token, but let's use the client credential approach for this time. First, as we're using Blazor WASM, we need a NuGet package to install.
- Microsoft.Identity.Client:
dotnet add package Microsoft.Identity.Client
After installing the package, add several environment variables to local.settings.json
. Here are the details for authentication.
{ | |
"Values": { | |
"LoginUri": "https://login.microsoftonline.com/", | |
"TenantId": "<tenant_id>", | |
"ClientId": "<client_id>", | |
"ClientSecret": "<client_secret>", | |
"ApiHost": "https://graph.microsoft.com/", | |
"BaseUrl": "v1.0/" | |
} | |
} |
To get the access token, write the code below. Without having to worry about the user interaction, simply use both ClientID and ClientSecret values, and you'll get the access token. For example, if you use the ConfidentialClientApplicationBuilder
class, you'll easily get the one (line #16-20).
private async Task<string> GetAccessTokenAsync() | |
{ | |
var apiHost = Environment.GetEnvironmentVariable("ApiHost"); | |
var scopes = new [] { $"{apiHost.TrimEnd('/')}/.default" }; | |
var options = new ConfidentialClientApplicationOptions() | |
{ | |
Instance = Environment.GetEnvironmentVariable("LoginUri"), | |
TenantId = Environment.GetEnvironmentVariable("TenantId"), | |
ClientId = Environment.GetEnvironmentVariable("ClientId"), | |
ClientSecret = Environment.GetEnvironmentVariable("ClientSecret"), | |
}; | |
var authority = $"{options.Instance.TrimEnd('/')}/{options.TenantId}"; | |
var app = ConfidentialClientApplicationBuilder | |
.Create(options.ClientId) | |
.WithClientSecret(options.ClientSecret) | |
.WithAuthority(authority) | |
.Build(); | |
var result = await app.AcquireTokenForClient(scopes) | |
.ExecuteAsync() | |
.ConfigureAwait(false); | |
var accessToken = result.AccessToken; | |
return accessToken; | |
} |
Once you have the access token in hand, you can use Microsoft Graph API.
Microsoft Graph API for .NET
To use Microsoft Graph API, install another NuGet package:
- Microsoft.Graph:
dotnet add package Microsoft.Graph
And here's the code to get the Graph API. Call the method written above, GetAccessTokenAsync()
(line #4-8).
private async Task<GraphServiceClient> GetGraphClientAsync() | |
{ | |
var baseUri = $"{Environment.GetEnvironmentVariable("ApiHost").TrimEnd('/')}/{Environment.GetEnvironmentVariable("BaseUrl")}"; | |
var provider = new DelegateAuthenticationProvider(async p => | |
{ | |
var accessToken = await this.GetAccessTokenAsync().ConfigureAwait(false); | |
p.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); | |
}); | |
var client = new GraphServiceClient(baseUri, provider); | |
return await Task.FromResult(client).ConfigureAwait(false); | |
} |
Finally, call the GetGraphClientAsync()
method to create the Graph API client (line #1) and get the user details using the email address taken from the ClientPrincipal
instance (line #4). If no user data is queried, you can safely assume that the email address used for the ASWA log-in is not registered as either a Guest User or an External User. Therefore, the code will return the 404 Not Found
response (line #7).
var client = await this.GetGraphClientAsync().ConfigureAwait(false); | |
var users = await client.Users.Request().GetAsync().ConfigureAwait(false); | |
var user = users.SingleOrDefault(p => p.Mail == userEmail); | |
if (user == null) | |
{ | |
return new NotFoundResult(); | |
} |
The amount of your information would be huge if you could filter out your details from AAD.
{ | |
"accountEnabled": null, | |
"ageGroup": null, | |
"assignedLicenses": null, | |
... | |
"displayName": "Justin Yoo", | |
... | |
"givenName": "Justin", | |
... | |
"mail": "justin.yoo@<external_tenant_name>.onmicrosoft.com", | |
... | |
"surname": "Yoo", | |
"usageLocation": null, | |
"userPrincipalName": "justin.yoo_<external_tenant_name>.onmicrosoft.com#EXT#@<tenant_name>.onmicrosoft.com", | |
... | |
} |
You don't want to expose all the details to the public. Therefore, you can create another POCO class only for the necessary information.
public class LoggedInUser | |
{ | |
public LoggedInUser(User user) | |
{ | |
this.Upn = user?.UserPrincipalName; | |
this.DisplayName = user?.DisplayName; | |
this.Email = user?.Mail; | |
} | |
[JsonProperty("upn")] | |
public virtual string Upn { get; set; } | |
[JsonProperty("displayName")] | |
public virtual string DisplayName { get; set; } | |
[JsonProperty("email")] | |
public virtual string Email { get; set; } | |
} |
And return the POCO instance to the Blazor WASM app side.
var loggedInUser = new LoggedInUser(user); | |
return new OkObjectResult(loggedInUser); |
Now, you've got the API to get the user details. Let's keep moving.
Exposing User Details on Azure Static Web Apps
Here's the code that the Blazor WASM app calls the API to get the user details. I use the try { ... } catch { ... }
block here because I want to silently proceed with the response regardless it indicates success or failure. Of course, You should handle it more carefully, but I leave it for now.
protected async Task<LoggedInUserDetails> GetLoggedInUserDetailsAsync() | |
{ | |
var details = default(LoggedInUserDetails); | |
try | |
{ | |
using (var response = await this._http.GetAsync("/api/users/get").ConfigureAwait(false)) | |
{ | |
response.EnsureSuccessStatusCode(); | |
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); | |
details = JsonSerializer.Deserialize<LoggedInUserDetails>(json); | |
} | |
} | |
catch | |
{ | |
} | |
return details; | |
} |
In your Blazor component, the method GetLoggedInUserDetailsAsync()
is called like below (line #6, 18).
<div class="page"> | |
... | |
<div class="main"> | |
... | |
<div class="top-row px-4 text-end"> | |
<span class="px-4">@DisplayName</span> | <a href="/logout">Logout</a> | |
</div> | |
... | |
</div> | |
</div> | |
@code { | |
protected string DisplayName; | |
protected override async Task OnInitializedAsync() | |
{ | |
var loggedInUser = await GetLoggedInUserDetailsAsync().ConfigureAwait(false); | |
DisplayName = loggedInUser?.DisplayName ?? "Not a registered user"; | |
} | |
} |
If your email address belongs to the tenant you want to query, you'll see the result screen like this:
If your email address doesn't belong to the tenant you want to query, you'll see the result screen like this:
Now, we can access your user details from the Blazor WASM app running on ASWA through Microsoft Graph API.
So far, I've walked through the entire process to get the user details:
- Using Blazor WASM app hosted on ASWA,
- Using MSAL for authentication and authorisation against AAD, and
- Using Microsoft Graph to access to the user details.
As you know, Microsoft Graph can access all Microsoft 365 resources like SharePoint Online, Teams and so forth. So if you follow this approach, your chances to use Microsoft 365 resources will get more broadened.