Reverse engineering Apple's MusicKitJS to create Apple Music Strategy for PassportJS
Tags: devjournal, passport, typescript, apple
Published: 2021-03-21 17:43:40

If you're wondering what's the gif in the title, that's a demo of authentication flow for Apple Music that I've tested in of the apps I've made, [MoovinGroovin](https://www.moovingroovin.com/).

This post describes my journey of isolating the authentication from [MusicKitJS](https://developer.apple.com/documentation/musickitjs/), and packaging it into a [passportJS](http://www.passportjs.org/) strategy.

And in case the cover is cropped, this is it:

![Apple Music Auth demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ok0q2svcwxs79hwcdtx.gif)
 

## TL;DR

The package is available as [passport-apple-music](https://www.npmjs.com/package/passport-apple-music) NPM package.

##Background

Over the past few months, I've made [MoovinGroovin](https://www.moovingroovin.com/), a web service that creates playlists from the songs you listened when working out with Strava turned on.

MoovinGroovin is integrated with Spotify, and I got a request from a user to add support for Apple Music.

Apple Music has a rest API (Apple Music API). The API has an endpoint to get recently-played resources which is what the app needed. But to access those on a per-user basis, you need a _music user token_. And these user tokens are made available only when the user logins in to Apple using MusicKitJS, and authenticates the app for Apple Music.

The MoovinGroovin app handled all other 3rd party integrations on the backend using [passportJS](http://www.passportjs.org/).

But, there was no passport strategy for Apple Music. And handling Apple Music on the frontend as an exception felt like a bad design choice.

And so my goal was to extract the MusicKitJS auth flow and package it into a passport strategy.

## Analysing MusicKitJS to understand auth flow

Note: To keep this article on the topic, I've written the tips for reverse-engineering minified JS code in a separate post:

{% post jurooravec/7-tips-for-reverse-engineering-minified-typescript-javascript-4p64 %}

When you trigger authentication using MusicKitJS, it opens a separate window with login page. This seems like a standard OAuth flow. Based on OAuth, I was expecting that I could ditch MusicKitJS by:

1. Generating the auth URL ourselves.
2. Passing in the callback URL (redirect URI) query param to the auth URL, which would point back to our server.
3. After user authenticates (or fails to do so) on the Apple login page, a request would be sent to the callback URL with either a `code` or `error` query param. `code` in this case, would've been the music user token we need.

Or so I thought..

## Generating auth URL

And so I searched and found how MusicKitJS generates the auth URL. And I found that the query params of the auth URL contain the encoded info of your [developer token](https://developer.apple.com/documentation/applemusicapi/getting_keys_and_creating_tokens), name and icon, as well as some analytics data.

You can see the query params in the comments in the screenshots below:

![Commented minified code of function _thirdPartyInfo from MusicKitJS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gchsoqj8258yvgy4yw5g.png)

![Commented minified code of function buildQueryParams from MusicKitJS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/42pcszn9iadchcmtc25g.png)

![Commented minified code of function _authorizeAction from MusicKitJS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mu2kd4jhe2lh02edfjzp.png)
 
As you can see, there's no magic to the auth URL. And, if you have the developer token, you can construct it yourself too.

Great! :parrot: So now I could just use that URL and call it a day...

## Controlling the auth flow

Except...

Except when I tried to open the URL, I got an error message and an error in the console. :sob:

![Error on Apple authentication page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/44eiqm5fmv6dufijqzrb.png)
 
![Console error of Apple authentication page with error about invalid URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/buq9n62dswxenl5p5p1n.png)

Quick debug revealed that the "invalid URL" was because the login page tries to do:

```js
new URL(document.referrer);
```

As can be seen in the last line here:

![Code showing the source of the invalid URL error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/65awpmlvcmi5ev3djkfa.png)
 
And if I open the page directly, well, there's no referrer! :facepalm: :facepalm:

_Cool, no problem_, I thought. But in fact it _was_ a problem.

First I tried to solve this by using an `iframe` and opening the auth URL _inside_ the `iframe`. However, I was still getting empty referrer.

So I had to stick with opening the auth URL in a separate window.

OK. _Fine_. But then it should work, right?

...I guess you already know the answer. :smirk:

This time, the auth URL loaded, I could enter my credentials, verify with 2FA, and confirm. But, then the page froze in a loading state.

So I did it again, and it froze again. I've checked if I got any response, and I did not.

So I tried again. Still nothing. :disappointed:

Once again diving deep into debugging the JS of the login page, it turned out that the login page sends messages using `postMessage`. And when it posts a message, it's waiting for a response with `addEventListener`. And if there's no response coming, it is stuck in the loading state.

Since I launched the window with the auth URL without handling any messages, it got stuck. 

And the referrer is expected to _respond_ to the sent messages. If this is not upheld, the JS on the auth URL hangs in a waiting state, and the user token is never received.

With an event-based communication like this, there could have been _a lot_ going on that I could miss if I tried to handle it instead of MusicKitJS.

That meant that I really did not want to - I could not - bypass MusicKitJS. I had to use it to handle the communication with the login page, so that I could then get the user token from MusicKitJS.

This meant that since we cannot bypass MusicKitJS, we have to work with the auth URL it generates. So we cannot pass the callback URL, or other additional query params to the auth URL. :worried:

To stick with the standard and to support passportJS features, I decided that, in the end, when we trigger the authentication for Apple Music, we need 2 windows:

The first is the one _we_ control, which triggers the `authorize` method on MusicKitJS, waits for login, and processes the response (user token) once authenticated. We can pass the callback URL to this window, and we know it will call it with either the user token or error.

The second is the window opened by MusicKitJS that hosts the auth URL and that is _out of our reach_.

## Passing options to auth flow

So at this point we've established how to trigger the Apple Music login page and get the user token back. Now, to make this into a _useful_ packaged passportJS strategy, we still need to enable to pass options through.

As seen earlier, we know that you can pass app name and icon to the auth window. For these, the process was fairly simple, since these can be passed to MusicKitJS during initialization.

However, the question remained how to pass the parameters to the URL that _we_ control.

One option was to require the user to add a server router endpoint. Upside of this is that we could pass all options as query params. BUT, then it would be user's responsibility to ensure that the right route is triggered. This is not the standard for passportJS strategies, where this should be abstracted away.

Other option was to respond directly with the HTML that will contain our logic to initialize MusicKitJS, call `authorize`, capture user token, and then redirect to the callback URL with the user token.

Since passportJS can be used with multiple frameworks, I didn't want to bake any templating engine into the package. Instead, the HTML string is constructed on the fly with vanilla `string.replace` to inject the user's options into the HTML. Not the cleanest solution, but hey, it works! :shrug:

![Options are passed to HTML through placeholders in a template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gek8jo54i8f5d4vv61fk.png)

![Placeholders in template are replaced with actual values using string.replace](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oku6p7hfqwu7fm0mbg14.png) 

...But as always, there's a catch 🤔

Spotify strategy has a [showDialog option](https://github.com/JMPerez/passport-spotify#forcing-login-dialog) that enable to force to show the login dialog. In my app, I was using that option show the dialog when users want to _change_ the associated account.

But once I was logged in into Apple Music, I was never prompted for login again.

Crawling through the MusicKitJS code _again_, I saw that `authorize` checks if user token already exists, so it _has_ to be stored somewhere.

![Commented minified code of function authorize from MusicKitJS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iszzwosq94z97qa8l2er.png)

And indeed, when I looked through the localStorage, there was an entry that started with `music.` and ended with `.u`. When the entry was there, the authorization was not trigger. when it was removed, I got asked to re-authorize on calling `authorize`.

Since we decided not to mess with/replace MusicKitJS, if we want to force the auth window to open, we need to make sure localStorage is free of Apple Music user tokens _before_ we call `authorize`.

So in the HTML that controls MusicKitJS, we also clean the relevant localStorage keys in prompted to:

![Code that removes music user tokens from localStorage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f7sqtman4ff0c1pzo84z.png)

---

## Conclusion

Not gonna lie, this was quite a journey. And not gonna lie, the Apple Music auth flow could've been design better, without the tight coupling to a parent window, and without the tight coupling to MusicKitJS.

Better design would lower the barrier for developers, and hence increase uptake - the number of developers trying out and authenticating with Apple Music. That would increase the number of apps or services that use the Apple Music API, which would improve Apple's positioning.

But luckily, now, the barrier has been lowered, and you can simply authenticate users against Apple Music. All you have to do is use [passport-apple-music](https://www.npmjs.com/package/passport-apple-music)! :tada: