MapKit JS with ASP.NET Core

Apple's MapKit JS isn't currently as widely used as some alternative web mapping solutions, but it's a reasonable option, depending on your needs. While there's no free version, the usage limits are very high, and it's priced more competitively than many rivals, since it's included with an Apple Developer Membership. Integrating MapKit JS with an ASP.NET Core application is a little different at first, if you've never worked with JWT, but it's actually pretty straightforward. The example below also only makes use of standard .NET libraries, and doesn't require any large third party libraries or any additional NuGet packages. This code should also work on Windows, macOS, and Linux. The full sample that goes along with this post is available on GitHub. (Note that there are some alternative Microsoft Cng libraries you could use -- but they're Windows only.)

MapKit JS makes use of JWT with an authorization callback, rather than an API key you might see with some competitors. So, we're going to create an API method that generates a JWT token for authorization. There are some steps you need to take on your Apple Developer account to get set up, so you'll also want to read Apple's documentation on creating a maps identifier and a private key and about some of the other values you need to supply to generate a token.

The example has spots in the appsettings.json & appsettings.Development.json for these values, but it does not include valid values, so it will not run as-is. You'll want to put your own values for PrivateKey (as a Base64 string -- you can take the p8 file you download from Apple, and copy/paste the private key out of the file with the line breaks removed), KeyIdentifier from the key you set up in your Apple Developer account, and TeamID that you can also grab from your Apple Developer account. The final value you'll want to change is Origin. MapKit JS will compare this value to the location the maps are hosted -- if it doesn't match, it'll throw an error. Note that you can omit Origin and MapKit JS won't validate your URL, but you probably won't want to do that for production and forgo the extra security check.

One of the other fields in the payload is the expiration -- our example has tokens that expire after 20 minutes. You can set the expiration to shorter or longer -- MapKit JS will call your authorization again if the token times out. Note that you will definitely want to create the JWT on your server -- you don't want to have your private key exposed anywhere on the web.

Below is the main piece of code in an API method which creates the JWT token:

[Route("api/mapkit/gettoken")]
public string GetMapKitToken()
{
    var header = new
    {
        // The encryption algorithm (alg) used to encrypt the token. ES256 should be used to
        // encrypt your token, and the value for this field should be "ES256".
        alg = "ES256",
        // A 10-character key identifier (kid) key, obtained from your Apple Developer account.
        // This is the value in after "AuthKey_" and before ".p8" in the private key file
        // you download from your Apple Developer Account
        kid = _mapKitSettings.KeyIdentifier,
        // A type parameter (typ), with the value "JWT".
        typ = "JWT"
    };
    var payload = new
    {
        // The Issuer (iss) registered claim key. This key's value is your 10-character Team ID,
        // obtained from your developer account.
        iss = _mapKitSettings.TeamID,
        // The Issued At (iat) registered claim key. The value of this key indicates the time at
        // which the token was generated, in terms of the number of seconds since UNIX Epoch, in UTC.
        iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
        // The Expiration Time (exp) registered claim key, in terms of the number of seconds since
        // UNIX Epoch, in UTC.
        exp = DateTimeOffset.UtcNow.AddMinutes(_mapKitSettings.TokenExpirationMinutes).ToUnixTimeSeconds(),
        // This key's value is a fully qualified domain that should match the Origin header passed
        // by a browser.  Apple compares this to your requests for verification.  Note that you can
        // omit this and get warnings, though it's definitely not recommended.
        origin = _mapKitSettings.Origin
    };
    var headerBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(header));
    var payloadBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload));
    var message = EncodingHelper.JwtBase64Encode(headerBytes)
        + "." + EncodingHelper.JwtBase64Encode(payloadBytes);
    var messageBytes = Encoding.UTF8.GetBytes(message);
    var crypto = ECDsa.Create();
    crypto.ImportPkcs8PrivateKey(Convert.FromBase64String(_mapKitSettings.PrivateKey), out _);
    var signature = crypto.SignData(messageBytes, HashAlgorithmName.SHA256);
    return message + "." + EncodingHelper.JwtBase64Encode(signature);
}

The below helper class has a single method that produces a Base64 string version of a byte array to the JWT specifications, which includes a few additional character replacements and one removal beyond the stock Base64 string conversion.

public static class EncodingHelper
{
    // Base64 Encode Per the JWT specifications
    public static string JwtBase64Encode(byte[] bytes)
    {
        if (bytes == null)
            throw new ArgumentNullException(nameof(bytes));
        if (bytes.Length == 0)
            throw new ArgumentOutOfRangeException(nameof(bytes));
        return Convert.ToBase64String(bytes)
            .Replace('+', '-')
            .Replace('/', '_')
            .TrimEnd('=');
    }
}

And below is a razor view with the MapKit initialization that takes advantage of our API method that generates a JWT token, sets up a map with a single marker annotation (pin) at Apple's headquarters, and finally fits the map to that location with a little bit of padding.

@{
    ViewData["Title"] = "Sample Home";
}
<div class="text-center">
    <h1 class="display-4">Sample Map</h1>
    <div id="mapContainer" style="height:400px"></div>
</div>
@section Scripts
{
    <script src="https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.js"></script>
    <script>
        mapkit.init({
            authorizationCallback: function(done) {
                fetch("/api/mapkit/gettoken")
                    .then(res => res.text())
                    .then(done);
            },
            language: "en"
        });
        var map = new mapkit.Map('mapContainer');
        // Create a balloon marker for Apple Park
        var appleParkCoordinates = new mapkit.Coordinate(37.28808, -122.01982);
        var annotation = new mapkit.MarkerAnnotation(appleParkCoordinates,
            {
                title: 'Apple Park'
            });
        map.addAnnotation(annotation);
        // Center and fit map on the annotation
        map.showItems([annotation],
            {
                padding: new mapkit.Padding(100, 100, 100, 100)
            });
    </script>
}

After replacing the configuration values with your own, you should be able to run the project and get a sample map like the screenshot below. The full sample is also posted on GitHub. Another tip for when you get a MapKit JS solution into production is that there is a MapKit JS dashboard that shows usage, lets you create one-off tokens, and lets you create snapshots. The link is hard to find, but it's https://maps.developer.apple.com

Here's a sample map using the code above:

Sample Map