Another yet article about JWT-based authentication for Apple APIs
By: Kirill Suslov
Tags: Apple Music, Programming, Ruby, Ios Development, iOS

If you came across this article, you most likely found several more before discovered mine and possibly even tried to implement Apple’s JWT-based auth as per their official docs.

I decided to write another one because I was one of the many other developers (1, 2 etc.) who spent hours struggling with 401 http response.

A short preface. I was playing around with Music API and I wanted to try out Apple music API. In order to do that I needed to authenticate all my requests with a developer token which is basically two json objects (header and body) encrypted using a quite specific algorithm with a private key that needs to be generated by Apple after finishing some button clicks in the Apple Dev Portal (or whatever it’s called).

The confusing part here is that Apple doesn’t provide a ready solution for lazy people so all that encrypting stuff needs to most likely be Googled. Here’s where I got into that very common trap: use first or second search result, copy-paste the solution without understanding what it is doing, fail and repeat searching for a next quick solution and get into the same trap again.

Here I am going to post the solution written in Ruby I came to (using solutions of other people in the internet obviously) and try to break it down so it becomes clear why it works. Note that all written here is how I processed the info so if I am wrong somewhere, just let me know in the comments. Also note that I mostly use Swift and Kotlin, so my Ruby skills are quite weak.

require "jwt" ISSUER_ID = "<issuer ID>"

KEY_ID = "<key ID>" payload = {

iss: ISSUER_ID,

iat: Time.now.to_i,

exp: Time.now.to_i + 60 * 60

} header = {

alg: "ES256",

kid: KEY_ID

} key_file = File.read("AuthKey_#{KEY_ID}.p8")

private_key = OpenSSL::PKey::EC.new key_file

# private_key.check_key token = JWT.encode(payload, private_key, "ES256", header_fields = header) puts "----TOKEN----"

puts token # public_key = OpenSSL::PKey::EC.new private_key

# public_key.private_key = nil

# decoded_token = JWT.decode token, public_key, true, { algorithm: 'ES256' }

# puts "Decoded token"

# puts decoded_token puts "----CURL----"

puts "curl -v -H 'Authorization: Bearer #{token}' \"https://api.music.apple.com/v1/catalog/us/songs/302053341\""

Alright, let’s begin with first two lines:

require "jwt"

require is a Ruby method that loads a given file once. In this particular case I load one library a.k.a. gem. jwt is a Ruby implementation of JWT standard (the one that Apple wants us to use). That gem support all the basic stuff needed to follow JWT spec: signing and bunch of encryprion/decription algorythms. In order to for you ruby script to actually load that gem, you need to do gem install jwt .

ISSUER_ID = "<issuer ID>"

KEY_ID = "<key ID>"

Next two lines: defining a constant (first letter should be capital to make it constant) that store issuer ID which is your team ID that you can find in Apple Dev Portal and key ID which is an identifier for a private key you’ve create there.

Next you create an object which is a Hash that stores a key-value info that Apple says it needs to be a payload. IDK the history there but I found it’s kinda neat that syntax of Hashes in Ruby looks like JSON.

payload = {

iss: ISSUER_ID,

iat: Time.now.to_i,

exp: Time.now.to_i + 60 * 60

}

We’re using three keys here ( iss , iat and exp ) because Apple needs to have it in the payload.

On the line two here: value of this entity is a constant defined above.

On the line three and four I am creating an instance on a class Time using a method now . now is an alias for a constructor ( new ) with no arguments so it will use the current system time. Then I call to_i function of Time which the value of time as an integer. Note: that for the exp value I also add 1 hour for the key to expire. As per Apple’s doc the longest the key can live is 6 months so that value can vary for you.

In the next three lines I create a header part of the token which has to have a hardcoded type of the algorithm used and the kid which is the team ID value from a constant above.

header = {

alg: "ES256",

kid: KEY_ID

}

Then the interesting part begins:

key_file = File.read("AuthKey_#{KEY_ID}.p8")

Since the private key you downloaded from dev portal is a file you need to read its content. In Ruby the simplest way to do that is to use File (module I assume)and its read method which accepts path to the file as argument. That method also opens and closes the file so it is just this one line.

Next line here is probably the most important. It is also a line which was the reason of my problem.

private_key = OpenSSL::PKey::EC.new key_file

So here’s what’s going on here. OpenSSL is a module that wraps a bunch of stuff to secure communications. Within that module there’s PKey module for asymmetric public key algorithms. It is too deep to dive into this area but briefly there’s of them in that module, one them which Apple wants to be used is EC (which stands for Elliptic Curve Cryptography). I personally have a really vague understanding on how it works and I for my task (to make API work) I don’t need to understand everything there, knowing how to use it and trusting that it works should be enough 🙂

EC is a class so here I call its constructor ( new ) which accepts a private key (the content of the file) as an argument. The return value is a private key that is used for encoding.

It is important here to use EC class because Apple wants us to use it. In the docs they specify it by full algo name: ECDSA . There two other classes PKey module contains, if you accidentally use those, you will still get a token generated, but Apple will throw 401 errors when you will use it. This was my first mistake: I copy pasted OpenSSL::PKey::RCA from a solution and I didn’t spend efforts checking and noticing that it is using a different algo.

There’s another way to generate private key by using read method that lives inside PKey module. This method can automatically detect the type of the key from file content which is quite neat. Since Apple already gives a correct ECDSA key then it’s probably a better approach.

private_key = OpenSSL::PKey.read(key_file)

Last small note here: in the official doc they provided a test key to simulate 429 response. I copied that key into created file and got a bad base64 decode error. This is how I learned that base64 encoding enforcing a line length limit of 64 characters so split the file content into lines if seeing errors here.

Next is getting the token to use with api requests.

token = JWT.encode(payload, private_key, "ES256", header_fields = header)

Here’s I am using JWT module that is coming from a gem I added before. From that module I am using encode function which accept 4 arguments.

First argument is payload which is required by JWT spec, second is private key that I read from a file and created above, third is the type of the encryption algorithm to be used (full list of supported algorithms is here) and the forth is header fields which are optional for the function but Apple requires them so we have to pass it here for Apple Music API to work.

Further you can see I commented out the part where I generate a public key you can use to decode token back into a payload and header. Leaving it as a home assignment to practice code reading 😉

Last thing here is couple of outputs:

puts "----TOKEN----"

puts token

to grab the token and paste into the code.

puts "----CURL----"

puts "curl -v -H 'Authorization: Bearer #{token}' \"https://api.music.apple.com/v1/catalog/us/songs/302053341\""

to quick test that authorization works. I borrowed that trick from my work Android app where I built a network interceptor that printed curl commands into logcat for a faster network debugging. That interceptor trick I borrowed from someone on the internet.

So why did I write this article? Before I started I had purposes and ideas, but when I started I just realized that I wanted to put all my thoughts into text. A lesson you might take from it is: