Places Diary #4: MapKit Tokens

Places uses Apple’s MapKit JS for maps and points of interest. MapKit JS requires a signed JSON Web Token (JWT) to authenticate against its API. I initially created an endpoint that I could load in the browser to retrieve a token that was good for a year. This made testing and development easy: save the token to a .env file on the server, forget about it for a year until it expires, then repeat.

Before opening Places up for more people to use, I wanted to:

  • Shorten the time the token was good for
  • Remove the need to manually update it
  • And constrain the token to specific domains prevent to others from using the token

Providing a short-lived token to MapKit JS

The MapKit JS docs outline the header fields and payload needed to generate a token and the jsonwebtoken package handles generating it. Set the expiration time to the number of seconds since UNIX Epoch when you want the token to expire. Date.now() / 1000 + (60 * 5) signs a token with a 5 minute expiration.

import jwt from "jsonwebtoken";

export async function getAuthorizationToken({ expiration = 60 * 5, origin }) {
  return jwt.sign(
    {
      iss: APPLE_DEVELOPER_TEAM_ID /* Issuer: This is a 10-character Team ID obtained from your Apple Developer account. */,
      iat: Date.now() / 1000 /* Issued at: Number of seconds since UNIX Epoch, in UTC */,
      exp: Date.now() / 1000 + expiration /* Expiration: When the token expires, in terms of the number of seconds since UNIX Epoch, in UTC */,
      origin: origin /* An optional claim that constrains the token to a specific website or domain. The value of this claim is a fully qualified domain, without a URL scheme */,
    },
    APPLE_MAPS_AUTH_KEY /* The private key that you obtain from your Apple Developer account */,
    {
      header: {
        kid: APPLE_MAPS_KEY_ID /* A 10-character key identifier that provides the ID of the private key that you obtain from your Apple Developer account. */,
        typ: "JWT" /* The type, “JWT” */,
        alg: "ES256" /* The algorithm you use to encrypt the token. Use the ES256 algorithm */,
      }
    }
  );
}

A endpoint is needed so the token can be retrieved. The route verifies the request is from an allowed origin, calls the getAuthorizationToken function constrained to the server’s hostname with a 5 minute expiration, and then returns it.

import { getAuthorizationToken } from "~/mapkit.server";

export async function loader({ request }) {
  const url = new URL(request.url);
  const serverHostName = process.env.SERVER_HOSTNAME;
  const allowedOrigins = ["localhost:3000", "example.com"];

  /** Get the fully qualified domain, without a URL scheme, from the request */
  const origin = url.origin.replace(/^https?:\/\//, "");

  /** Check if the origin is allowed */
  if (!allowedOrigins.includes(origin) || origin !== serverHostName) {
    return new Response("Request not allowed", { status: 403 });
  }

  /** Generate a new authorization token */
  const authorizationToken = await getAuthorizationToken({
    expiration: 60 * 5 /** five minutes */,
    origin: process.env.NODE_ENV === "development" ? undefined : process.env.SERVER_HOSTNAME /** `localhost` isn’t a valid origin, so omit this in development */,
  });

  return new Response(authorizationToken);
}

Now, instead of manually adding a token that needs to be updated when it expires, the authorizationCallback function MapKit JS requires for initialization can fetch a token from the endpoint. If MapKit JS detects an expired token, it will fetch a new one by calling the authorizationCallback function again.

mapkit.init({
  authorizationCallback: function(done) {
    fetch("/mapkit-token")
      .then(res => res.text())
      .then(done)
      .catch(error => {
        console.error(error.message, "Error fetching Apple Maps token");
      });
  },
});

Interested in trying Places?

If you are interested in trying Places, sign up for the waitlist and be among the first to know when it’s ready.

  1. Places Diary #1: An Introduction
  2. Places Diary #2: Removing Prisma
  3. Places Diary #3: Side Quests

All Posts