Search code examples
azureazure-active-directoryazure-maps

Azure Active Directory Token for Azure Map


I tried following example to implement the Azure Active Directory Token for Azure Map.

//Html
<html>
<body>
  <input type="button" onclick="LoadMapControl(this)" value="Load Map" />
  
  <div id="map"></div>
</body>
</html>

// Javascript
var map;

function LoadMapControl(elm) {
  //Only load the map if is hasn't been loaded. 
  if (!LazyMapLoader.IsLoaded()) {
    LazyMapLoader.LoadMapControl(GetMap);
  } else if (map == null) {
    GetMap();
  }

  //Hide the button
  elm.style.display = 'none';
}

function GetMap() {
  //Initialize a map instance.
  map = new atlas.Map('map', {
    view: 'Auto',

    //Add your Azure Maps subscription client ID to the map SDK.
    authOptions: {
      authType: "anonymous",
      clientId: "04ec075f-3827-4aed-9975-d56301a2d663", //Your Azure Maps account Client ID is required to access your Azure Maps account.

      getToken: function (resolve, reject, map) {
        //URL to your authentication service that retrieves an Azure Active Directory Token.
        var tokenServiceUrl = "https://azuremapscodesamples.azurewebsites.net/Common/TokenService.ashx";

        fetch(tokenServiceUrl).then(r => r.text()).then(token => resolve(token));
      }
    }
  });
}

//A reusable class that makes it easy to lazy load the Azure Maps Atlas map SDK.
var LazyMapLoader = new function () {
  var _callback = null, _isLoading = false;

  function isLoaded() {
    //Verify that the atlas namespace is loaded and that the Map class is recognized.
    return typeof (atlas) != 'undefined'
    && typeof (atlas.Map) != 'undefined';
  }

  this.LoadMapControl = function (callback) {
    var loaded = isLoaded();
    if (!_isLoading && !loaded) {
      _callback = callback;
      _isLoading = true;

      //Load the Atlas CSS Styles.
      var styles = document.createElement('link');
      styles.setAttribute('type', 'text/css');
      styles.setAttribute('rel', 'stylesheet');
      styles.setAttribute('href', 'https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css');
      document.body.appendChild(styles);

      //Load the Atlas JavaScript SDK.
      var script = document.createElement('script');
      script.setAttribute('type', 'text/javascript');
      script.setAttribute('src', 'https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.js');
      document.body.appendChild(script);
      setTimeout('LazyMapLoader.__LoadCallback();', 100);
    } else if (loaded) {
      _callback();
    }
  };

  this.IsLoaded = function () {
    return isLoaded();
  };

  this.__LoadCallback = function () {
    if (isLoaded()) {
      _callback();
      _isLoading = false;
    } else {
      setTimeout('LazyMapLoader.__LoadCallback();', 100);
    }
  };
};

// Css
html,
body {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
}

#map {
  width: 100%;
  height: 100%;
}

I understand we need to put in our authentication service rather than "https://azuremapscodesamples.azurewebsites.net/Common/TokenService.ashx". Is there anyone who knows how to do this in Azure Portal?

TokenServiceUrl

Because it's unauthorized, I'm getting the following error.

Error

I would appreciate it if someone could explain how to implement an Azure Active Directory Token for Azure Map.


Solution

  • You can create a simple Azure function or if using Azure Web apps, add a simple service in there to get the token.

    Here is the code I use in an HTTP trigger Azure function for one of my apps to get a token (written in C#):

    using System;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Azure.WebJobs;
    using Microsoft.Azure.WebJobs.Extensions.Http;
    using Microsoft.AspNetCore.Http;
    using Azure.Core;
    using Azure.Identity;
    
    namespace Microsoft.GetAzureMapsToken
    {
        public static class GetAzureMapsToken
        {
            /// <summary>
            /// This token provider simplifies access tokens for Azure Resources. It uses the Managed Identity of the deployed resource.
            /// For instance if this application was deployed to Azure App Service or Azure Virtual Machine, you can assign an Azure AD
            /// identity and this library will use that identity when deployed to production.
            /// </summary>
            /// <remarks>
            /// This tokenProvider will cache the token in memory, if you would like to reduce the dependency on Azure AD we recommend
            /// implementing a distributed cache combined with using the other methods available on tokenProvider.
            /// </remarks>
            private static readonly DefaultAzureCredential tokenProvider = new();
    
            ///A list of any domains you want to restrict access from. This is optional and a secondary security measure.
            private static readonly string[] allowed = { 
                "https://my-custom-domain.com/"
            };
    
    
            [FunctionName("GetAzureMapsToken")]
            public static async Task<IActionResult> Run(
                [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req)
            {
                 string referer = req.Headers["Referer"];
    
                if (string.IsNullOrEmpty(referer))
                    return new UnauthorizedResult();
    
                string result = Array.Find(allowed, site => referer.StartsWith(site, StringComparison.OrdinalIgnoreCase));
                if (string.IsNullOrEmpty(result))
                    return new UnauthorizedResult();
    
                // Managed identities for Azure resources and Azure Maps
                // For the Web SDK to authorize correctly, you still must assign Azure role based access control for the managed identity
                // https://docs.microsoft.com/en-us/azure/azure-maps/how-to-manage-authentication
                var accessToken = await tokenProvider.GetTokenAsync(
                    new TokenRequestContext(new[] { "https://atlas.microsoft.com/.default" })
                );
    
                return new OkObjectResult(accessToken.Token);
            }
        }
    }
    

    Then the URL I use to call this service is something like this:

    https://<My Azure function domain>.azurewebsites.net/api/GetAzureMapsToken
    

    In order for the function to access your Azure Maps account, you need to "Add role assignment" to the Azure Maps account. This is how you do that:

    1. Go into the Azure portal, go to your Azure Maps account resource.
    2. Select "Access control (IAM)".
    3. Click on "Role assignments".
    4. Click "Add", then "Add role assignment".
    5. Select a role type. You most likely want one of the reader type roles.
      • "Reader" provides access to all your Azure Maps API's including the data APIs of the Azure Maps Creator Maps platform (indoor maps and custom data you uploaded).
      • "Azure Maps Data Reader" provides access just to the Azure Maps creator platform.
      • "Azure Maps Search and Render data reader" provides access to all by the Azure Maps Creator platform. In most cases you will want this or the "Reader" role.
    6. Click next, then under Members, select "User, group, or service principal".
    7. Then press "select members" and search for your search for your azure function. Select it and press next, then review and assign.

    It might take 5 minutes for this to propagate through the systems, so don't stress if it doesn't work right away. This has caught me every time.

    Finally, make sure you update the clientId in your JavaScript code with the value in your Azure Maps account (you can find it under the "Authentication" section of your Azure Maps account resource in the portal).