Sun May 03 2026
Welcome to Part II of my River of Babel post mortem. Today I’ll be doing a break down on how I used JWTs (JSON Web Tokens) to interact with the MusicKit API. If you haven’t read my first piece on why I shut down River of Babel in the first place, you should check it out here before you jump into this blog.
As a reminder: I used Nuxt as my main framework in my stack, and the interaction between this and MusicKit is what made things so difficult.
Great question. In the barest sense, a JSON Web Token is a method for validating an API request using a randomly generated token encoded in a JSON. In more technical terms, here is the excerpt for the JWT RFC:
“A string representing a set of claims as a JSON object that is encoded in a JWS or JWE, enabling the claims to be digitally signed or MACed and/or encrypted.”
The scheme is pretty neat, but I found interacting with it in the context of Music Kit via my Nuxt stack an absolute nightmare.
My goal was to be able to generate a developer token, which in turn would allow me to query the Apple MusicKIt API which I would then use to “translate” music links between one another.
The issue here is that Apple’s documentation was not exactly helpful. To make matters more complicated, I would have to use the Nuxt Server to generate the token, then I would have to pass the token to the frontend.
To accomplish this, I would also have to leverage Pinia, which is a Vue Store framework. Pinia is very powerful, and I cannot recommend it enough.
I would need token handling in the following places:
Let’s dive in.
Before I did anything, I had to create my apple api secret, and my KID. How to do this thankfully decently documented on Apple’s site, so I won’t go into that here except to say that this information is stored in a .env file stylized like VITE_APPLE_API_SECRET so that the nuxt app can read it.
VITE_APPLE_API_SECRET=”-----BEGIN PRIVATE KEY-----
Your key goes here
-----END PRIVATE KEY-----"
VITE_APPLE_KID=”yourkidhere”
You will also need on hand your app id from the Aapple Developer portal.
In my nuxt api server route, I made a file called getJWT.ts . Additionally, I also installed the jose npm package.
Basically, this code will generate a valid jwt.
import crypto from "node:crypto";
import * as jose from "jose";
export default defineEventHandler(async (event) => {
const privateKey = process.env.VITE_APPLE_API_SECRET
const keyObject = crypto.createPrivateKey(privateKey);
const epoch = Math.floor(Date.now() / 1000);
let claims = {
iss: "",
iat: epoch,
exp: epoch + 60 * 60 * 24 * 7,
origin: "http://localhost:3000",
};
const jwt = await new jose.SignJWT({})
.setProtectedHeader({
alg: "ES256",
kid: process.env.VITE_APPLE_KID,
})
.setIssuer("/*Your App ID*/")
.setIssuedAt()
.setExpirationTime("1 Week")
.sign(keyObject);
return jwt;
});
Here we’re using the built in cryptography functionality from node to encode our private key. We then use jose to create a signed JWT. The code is pretty self explanatory here.
To test your JWT is valid use this website: https://www.jwt.io/
Of course, nothing involving this is easy - and Nuxt specifically had an issue importing the JWT on the client side, so we have to create two plugins to help us handle this. I got this information from a very helpful stack overflow post.
Honestly, I cannot really remember how these fit into the entier picture, but I know that the app works with them. So here ya go. First, in the plugin directory in nuxt, we have generateJWT.server.ts.
import jwt from 'jsonwebtoken';
export default defineNuxtPlugin((nuxtApp))=>{
return {
provide:{
generateJWT: ()=>{
return jwt.sign({
alg:'ES256',
kid: '6598SC5394',
typ: 'JWT'
}, process.env.VITE_APPLE_API_SECRET, { algorithm: 'RS256' })
}
}
}
}
Then we have a plug in that verifies the JWT is valid called jwt.server.ts
import jwt from 'jsonwebtoken';
export default defineNuxtPlugin((nuxtApp) => {
return {
provide: {
verifyJwtToken: (token: string, secret: string, options: object) => {
return jwt.verify(token, secret, options);
}
}
}
})
This stuff may be completely irrelevant and dead code that is never touched - honestly I am not sure. I wrote this a year ago. This is why you document your code, kids!
I used to the token store to keep both Apple and Spotify tokens, but I’ll only put the code here that is relevant to the apple tokens. You can read the entire code on my github if you feel like it!
export const useTokenStore = defineStore('token', () => {
///irrelvant
const appleToken = ref("")
const lastAppleFetched = ref("")
const appleExpiryTime = ref(3599)
function setAppleToken (newToken: String) {
appleToken.value = newToken;
}
function getAppleToken(){
return appleToken
}
function setAppleLastFetched(lastFetchedTime:Date){
lastAppleFetched.value = lastFetchedTime;
}
function getAppleLastFetched(){
return lastAppleFetched
}
return {setAppleToken, getAppleToken, setAppleLastFetched, getAppleLastFetched}
});
Again, this code has Spotify things involved, but I am only highlighting the relevant Apple stuff.
<script setup>
const store = useTokenStore()
const emit = defineEmits(['tokensStored'])
const appleTokenStored = ref(false)
async function handleAppleTokens() {
const { data } = await useFetch('/api/getJWT')
store.setAppleToken(data.value)
//TODO: if expired refetch
//Note: I never did the above TODO
appleTokenStored.value = true
}
watch(() => appleTokenStored.value === true, () => { emit('tokensStored') })
try{
await handleAppleTokens()
} catch (err){
console.log(err)
window.location.reload()
}
</script>
This code is pretty fun, I think. Basically, we have an asynchronous function handleAppleTokens() that calls the JWT server route we wrote earlier. We get the JWT and store it in the store. We have a watcher that emits a signal whenever the tokens are stored, which we then use to tell our website that we’re good to start querying.
And there you have it!
With this, River of Babel is officially dead. Long live River of Babel! I have archived the code, which you can see here. Until next time - peace out!