MS Graph, Blazor WebAssembly and Azure Static Web Apps

This post has been republished via RSS; it originally appeared at: New blog articles in Microsoft Tech Community.

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:

 

Before log-in

 

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:

 

  1. You logged into a tenant, and
  2. 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.

 

 

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:

 

 

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 target="_blank" 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:

 

After the log-in - user found

 

If your email address doesn't belong to the tenant you want to query, you'll see the result screen like this:

 

After the log-in - user not found

 

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:

 

 

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.

 

This article was originally published on Dev Kimchi.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.