AAD Login HoloLens2 - MRTK3 Update

6 minute read

Eye

What's the first thing that anyone creating any enterprise app will need to work out?

**Auth**.

You can't really create an app that will actually be used in a real scenario without having thought this through. Embedding access keys into your app won't cut it and we often need to be able to support multiple users in a secure way.

The only secure way to share a HoloLens device is to use AAD identities https://docs.microsoft.com/en-us/hololens/hololens-identity#lets-talk-about-setting-up-user-identity-for-hololens-2. Alongside this you can authenticate using iris recognition. I wrote a previous post around that here https://petexr.com/aad/azure/hololens/login/mixedreality/oauth2/uncategorized/2020/07/17/aad-login-on-hololens-2.html where I also covered other auth methods including the use of MSAL. This time I will focus on using the auth token that is already present on a HoloLens device if you re using AAD to login to the device itself. In addition, I neglected to cover a common scenario which is using the token to protect a custom web API.

So, we will walk through setting up the Azure side of things and be left with a sample that will retrieve the token without the need to re-authenticate and use that token to access a custom web API. I'm going to use MRTK3 and Unity 2021.3 and the code will be using WebAuthenticationCoreManager which is UWP-only and so this solution is not cross-platform in any way and won't run in the Unity editor. If you are new to MRTK you can find mrtk3 on a branch (mrtk3) at that repository. For convenience, I would suggest using device code flow in the editor which you can easily implement using MSAL (See https://petexr.com/hololens/mixedreality/msgraph/oauth2/2019/01/17/microsoft-graph-auth-on-hololens-device-code-flow.html).

I haven't checked but it could be that MSAL uses similar on HoloLens now so this could be part of a cross-platform solution.

Custom Web API

So, to illustrate we can just use the boilerplate web API project that Visual Studio generates for us:

Visual Studio New Project

Configure it to use Windows.Identity middleware:

Identity Platform

Note. before publishing the Web API I made a single change:

I added a single `[Authorize]` attribute on the containing Controller class:

namespace aad_hl2_webapi.Controllers
{
    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {

This will activate the Auth middleware for web calls into this class and ensure the entry points get called with a valid OAuth token or a `401 Unauthorized` will get returned.

Publish

And we'll just publish it to an Azure app Service. I quite like the wizard in Visual Studio that helps with this job:

publish

After following instructions to configure resource groups and app service settings we can publish the web API with a click.

App Service

The Web API has a single GET which returns a random weather forecast:

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
}

One final thing to configure for the API itself are the App settings which look like this in the local appsettings.json file:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "qualified.domain.name",
    "TenantId": "22222222-2222-2222-2222-222222222222",
    "ClientId": "11111111-1111-1111-11111111111111111"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

I don't intend to run the API locally so won't edit these here but instead will need to set these settings up for the deployed API. I'll come back to this later when I know what the AAD app registration settings are that I can use.

AAD App Registrations

We are going to need two AAD App registrations for this:

AAD App Registration

- One for the Web API. This exposes some API scopes that I created for the purpose of illustration.

Web API App Registration

- One for the UWP HoloLens app. This declares the scopes from the web API

HoloLens App Registration

Configure the Web API

We need to configure the Web API with the relevant settings from the web API app registration.

Web API App Registration Properties

And we're going to set those directly as properties on the deployed App Service:

Deployed Properties

Note. My Web API is deployed to a different subscription to the AAD I am using.

Note. I have the following settings:

> **AzureAd:Audience** This is set to the Application ID URI from the hl2webapi app registration. (I'm not 100% if this is needed or not but suspect it is)

> **AzureAd:ClientId** This is set to the Application ID URI from the hl2webapi app registration. (My token didn't work until I set this to be the api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx format of the Application ID Uri and it took me a while to figure this out!)

> **AzureAd:Instance** This is set to https://login.microsoftonline.com/

> The rest are self-explanatory.

Unity HoloLens App

In order to retrieve the AAD token from the HoloLens device we can use the following code (with error handling and logging removed for brevity):

public async void Login()
{
#if ENABLE_WINMD_SUPPORT
    WebAccountProvider wap =
        await WebAuthenticationCoreManager.FindAccountProviderAsync("https://login.microsoft.com", Authority);

    WebTokenRequest wtr = new WebTokenRequest(wap, string.Empty, ClientId.ToString());
    wtr.Properties.Add("resource", Resource);
    
    WebTokenRequestResult tokenResponse = await WebAuthenticationCoreManager.GetTokenSilentlyAsync(wtr);
    
    if (tokenResponse.ResponseStatus == WebTokenRequestStatus.Success)
    {
        foreach (var resp in tokenResponse.ResponseData)
        {
            var name = resp.WebAccount.UserName;
            AccessToken = resp.Token;
            var account = resp.WebAccount;
            Username = account.UserName;
        }

        Debug.Log($"Access Token: {AccessToken}");
    }
#endif
}

We are getting close now....

I nearly forgot that we also need to configure a Redirect URI.

If you execute the following code on the HoloLens you can log out a value you can use as the redirect URI:

string URI = string.Format("ms-appx-web://Microsoft.AAD.BrokerPlugIn/{0}",
WebAuthenticationBroker.GetCurrentApplicationCallbackUri().Host.ToUpper());

So, copy that value and head back over to the HoloLens AAD app registration and add it as a redirect URI:

Redirect URI

So, unless I have forgotten anything important, you should now be able to run the HoloLens app logged in with any user in your AAD tenant, and have it successfully call the weather API. To illustrate this a bit I have fleshed out my sample a bit using MRTK3.

MRTK3 Sample

Unity Scene

So, the gist of this is that there is a menu with two buttons; one to retrieve the auth token and the other to issue a call to the weather API using that token to gain access.

The results will be data bound using MRTK3's new data binding stack.

Unity Player

There is a WebAPiDataSource script based on the MRTK3 data binding samples:

using Microsoft.MixedReality.Toolkit.Data;
using Microsoft.MixedReality.Toolkit.UX;
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class WebApiDataSource : DataSourceGOBase
{
    public delegate void RequestSuccessDelegate(string jsonText, object requestRef);
    public delegate void RequestFailureDelegate(string errorString, object requestRef);

    public DataSourceJson DataSourceJson { get { return DataSource as DataSourceJson; } }

    [Tooltip("URL for a custom Web API")]
    [SerializeField]
    private string url = "https://aad-hololens-api.azurewebsites.net/weatherforecast";

    [Tooltip("Auth data")]
    [SerializeField]
    private Auth auth;

    [Tooltip("Dialog Prefab")]
    [SerializeField]
    private Dialog DialogPrefabSmall;

    /// <summary>
    /// Set the text that will be parsed and used to build the memory based DOM.
    /// </summary>
    /// <param name="jsonText">THe json string to parse.</param>
    public void SetJson(string jsonText)
    {
        DataSourceJson.UpdateFromJson(jsonText);
    }

    /// <inheritdoc/>
    public override IDataSource AllocateDataSource()
    {
        return new DataSourceJson();
    }

    public void FetchData()
    {
        if (string.IsNullOrEmpty(auth.AccessToken))
        {
            Dialog.InstantiateFromPrefab(DialogPrefabSmall,
                new DialogProperty("Error", "No token to call the API with.",
                DialogButtonHelpers.OK), true, true);
        }

        StartCoroutine(StartJsonRequest(url, auth.AccessToken));
    }
    
    public IEnumerator StartJsonRequest(string uri, string accessToken, 
        RequestSuccessDelegate successDelegate = null, RequestFailureDelegate failureDelegate = null, 
        object requestRef = null)
    {
        using (UnityWebRequest webRequest = UnityWebRequest.Get(uri))
        {
            webRequest.SetRequestHeader("Authorization", "Bearer " + accessToken);
                
            // Request and wait for the desired page.
            yield return webRequest.SendWebRequest();

#if UNITY_2020_2_OR_NEWER
            if (webRequest.result == UnityWebRequest.Result.ProtocolError || 
                webRequest.result == UnityWebRequest.Result.ConnectionError)
#else
                if (webRequest.isHttpError || webRequest.isNetworkError)
#endif
            {
                if (failureDelegate != null)
                {
                    failureDelegate.Invoke(webRequest.error, requestRef);
                }
            }
            else
            {
                string jsonText = webRequest.downloadHandler.text;

                DataSourceJson.UpdateFromJson(jsonText);
                if (successDelegate != null)
                {
                    successDelegate.Invoke(jsonText, requestRef);
                }
            }
        }
    }
}

And providing you have hooked up scripts for defining data consumers (in this case a Data Consumer Collection). For me this looks like this:

MRTK3 Data Binding Components

Then you can provide a prefab as an item template to represent each item in the list:

Item Template

and you are good to go!

The source for the sample can be found here.

Comments