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:
.p8 file, including all newlinesBEGIN and END linesNone 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.
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":

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

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

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:

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:

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

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

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.

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)