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.
Other posts in this series
All Posts