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)