Search code examples
androidapple-musickit

What Do We Use For the Android MusicKit Developer Token?


I am attempting to use Apple's MusicKit SDK for Android. Adding their authorization SDK went fine, and I can launch the Apple Music app to request authorization. However, I keep getting TOKEN_FETCH_ERROR in the response, suggesting that Apple does not like my developer token.

In my call to createIntentBuilder() on Apple's AuthenticationManager, where I am supposed to pass my developer token, what exactly is that token?

The .p8 file that I got from developer.apple.com is an encoded certificate, with four encoded lines between the -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----. I am under the impression that we are to use this .p8 file as the developer token, but I have tried each of the following:

  • The verbatim contents of the .p8 file, including all newlines
  • The contents consolidated onto one encoded line between the BEGIN and END lines
  • Just the encoded contents, all on one line

None seem to work — they all give me TOKEN_FETCH_ERROR responses. The key itself is enabled for "Media Services (MusicKit, ShazamKit, Apple Music Feed)", which feels like the right option.

interface AppleMusicAuth {
    sealed interface AuthResult {
        data class Success(val musicUserToken: String) : AuthResult
        data object Failure : AuthResult
    }

    fun buildSignInIntent(): Intent
    fun decodeIntentResult(intent: Intent?): AuthResult
}

class AppleMusicAuthImpl(private val context: Context) : AppleMusicAuth {
    private val authManager = AuthenticationFactory.createAuthenticationManager(context)

    override fun buildSignInIntent() =
        authManager.createIntentBuilder(context.getString(R.string.developer_token))
            .setHideStartScreen(true)
            .setStartScreenMessage("hi!")
            .build()

    override fun decodeIntentResult(intent: Intent?): AppleMusicAuth.AuthResult {
        val rawResult = authManager.handleTokenResult(intent)

        return if (rawResult.isError) {
            Log.e("test", "Error from Apple Music: ${rawResult.error}")

            AppleMusicAuth.AuthResult.Failure
        } else {
            Log.d("test", "musicUserToken ${rawResult.musicUserToken}")

            AppleMusicAuth.AuthResult.Success(rawResult.musicUserToken)
        }
    }
}

In the above code, I am going through the rawResult.isError branch, and the log message shows that the error is TOKEN_FETCH_ERROR.


Solution

  • This answer tries to provide an end-to-end explanation of how to get a MusicKit developer token on Android. These instructions were accurate as of 2 August 2024. Things tied to Apple's Web site might have changed by the time that you read this. The code snippets were based on JJWT version 0.12.6.

    Step #1: Set up an Apple developer account at https://developer.apple.com/

    Step #2: Visit https://developer.apple.com/account, scroll down to the "Membership details" section, and note what value you have for "Team ID". This value appears to be semi-secret, so you may want to treat it akin to how you treat other secret values in your code base.

    Step #3: On that same page, go to the "Certificates, IDs & Profiles" section and click on "Identifiers":

    Membership details section, with Certificates highlighted

    Step #4: Click the "+" sign next to "Identifiers":

    Identifiers page, + sign highlighted

    Step #5: In the long list of identifier types, choose "Media IDs" and click the "Continue" button:

    Media IDs identifier option

    Step #6: In the "Register a Media ID" page, choose "MusicKit", fill in a suitable description, fill in a suitable identifier (following their instructions), and click "Continue". You can then click "Register" after confirming what you filled in to actually create the ID:

    Register a Media ID page

    Step #7: After Step #6, you should have wound up back at the Identifiers page from Step #3, except that your identifier should appear there. If you wound up somewhere else, follow those first few steps to get back into the "Certificates, Identifiers & Profiles" page.

    Step #8: Click on "Keys" to visit the Keys page, then click the "+" button:

    Keys page, + sign highlighted

    Step #9: On the "Register a New Key" page, check the "Media Services" option, then click the "Configure" button:

    Register a New Key page, Configure button highlighted

    Step #10: On the "Configure Key" page, choose your identifier from Step #6 in the drop-down, then click Save:

    Configure Key page

    Step #11: You should have wound up back on the "Register a New Key", with the "Configure" button now changed to an "Edit" button. Fill in a key name, then click "Continue". Then click "Register" after confirming what you filled in.

    Step #12: You should wind up on a "Download Your Key" page. Click "Download" to download a .p8 file, and save it somewhere private and safe. Click "Done" when you are done.

    Download Your Key

    Step #13: This should lead you back to the Keys page from Step #8, with your new key in the list. Click on it to view the key details. Note what value you have for "Key ID". This value appears to be semi-secret, so you may want to treat it akin to how you treat other secret values in your code base.

    Step #14: Make a copy of the .p8 file, then edit that copy in a plain text editor (e.g., Sublime Text). Remove the -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- lines and remove all newlines, so the seemingly-random text (actually some base64-encoded data) all appears on one line. Store this encoded private key akin to how you treat other secret values in your code. At this point, you have a total of three secrets: the team ID, the key ID, and the encoded private key (derived from the .p8 file).

    Step #15: Add the JJWT library to your app.

    Step #16: Create a function akin to the following:

        private fun buildDeveloperToken(p8: String, keyId: String, teamId: String): String {
            val priPKCS8 = PKCS8EncodedKeySpec(Decoders.BASE64.decode(p8))
            val appleKey = KeyFactory.getInstance("EC").generatePrivate(priPKCS8)
            val now = Instant.now()
            val expiration = now.plus(120, ChronoUnit.DAYS)
    
            val jwt = Jwts.builder().apply {
                header()
                    .add("alg", "ES256")
                    .add("kid", keyId)
                claim("iss", teamId)
                issuedAt(Date.from(now))
                expiration(Date.from(expiration))
            }.signWith(appleKey).compact()
    
            return jwt
        }
    

    Here, p8 is the encoded private key derived from the .p8 file, keyId is the key ID, and teamId is the team ID. This code decodes p8 into a PrivateKey, builds a JWT token using the keyId and teamId, and signs it with the PrivateKey, following Apple's documentation. Note that this code sets the lifetime of the signature to be 120 days — the maximum is six months.

    You can then use that buildDeveloperToken() function in things like createIntentBuilder():

    fun buildSignInIntent() =
            AuthenticationFactory.createAuthenticationManager(context)
                .createIntentBuilder(buildDeveloperToken(...))
                .setHideStartScreen(false)
                .setStartScreenMessage("i can haz ur muzik?")
                .build()
    

    (where ... is code that retrieves those three secrets from wherever you are storing them)