Exit
  • Global community
    • Language:
      • Deutsch
      • English
      • Español
      • Français
      • Português
  • 日本語コミュニティ
  • 한국 커뮤니티
0

Generating JWT Token for Apple API Requests

Engaged ,
Aug 05, 2022 Aug 05, 2022

Has anyone successfully generated a valid token for Apple's App Store API using ColdFusion?

We are following the instructions from Apple and cannot get the damn thing to authenticate.  Every call returns "401 Unauthorized ".

 

We're using a JWT CFC code found here the uses Java for signing the JWT.

 

The curious thing is, we've used this same CFC to successfully sign a JWT (slightly different payload) for use with Apple's MapKitJS API.  But, for reasons unknown, we can't get the App Store API to authenticate.

 

Any help is appreciated.

 

Our code sample:

<cfsavecontent variable="ourP8Key">
-----BEGIN PRIVATE KEY-----
Our
Key
Data
Here
-----END PRIVATE KEY-----
</cfsavecontent>

<!--- 'kid' value is private key ID from App Store Connect --->
<cfset tokenHeader = {
	"kid":"OURPRIVATEKEY"
}>

<!--- iss value: issuer ID from the Keys page in App Store Connect --->
<cfset tokenPayload = {
	"iss":"OUR-ISS-ID",
	"iat":now(),
	"exp":dateAdd("n",20,now()),
	"aud":"appstoreconnect-v1",
	"bid":"com.our.app"
}>

<cfset jwt = createobject("component","cfc.jwt.jwt").init()>

<cfset token = jwt.encode(tokenPayload, ourP8Key, 'ES256',tokenHeader)>

<cfhttp url="https://api.storekit-sandbox.itunes.apple.com/inApps/v1/history/#transactionid#" method="get" result="apiRes">
	<cfhttpparam type="header" name="Authorization" value="Bearer #token#">
</cfhttp>

"

2.0K
Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines

correct answers 1 Correct answer

Engaged , Aug 09, 2022 Aug 09, 2022

OMG!  I figured it out!

First off - the main issue was the 'exp' and 'iat' values do need to be INT

 

The problem was the jwt.cfc was converting my dates to strings.  So, instead of passing CF dates into the CFC, I converted them to epoch time using an old 'epoch' function I had laying around:

<cfset tokenPayload = {
		"iss":"my-iss-code",
		"iat":int(epochTime(now())),
		"exp":int(epochTime(dateAdd("n",20,now()))),
		"aud":"appstoreconnect-v1",
		"bid":"com.my.app"
	}>

<cffunction name="epochTime"
...
Translate
Community Expert ,
Aug 07, 2022 Aug 07, 2022

Try

<cfhttp url="https://api.storekit-sandbox.itunes.apple.com/inApps/v1/history/#transactionid#" method="get" result="apiRes">
    <cfhttpparam type="header" name="alg" value="ES256>
    <cfhttpparam type="header" name="kid" value="your_KID_key">
    <cfhttpparam type="header" name="typ" value="JWT">
    <cfhttpparam type="header" name="Authorization" value="Bearer #token#">
</cfhttp>
Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Engaged ,
Aug 08, 2022 Aug 08, 2022

Unfortunately, your suggestion did not work.  Thank you for the reply.

Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 08, 2022 Aug 08, 2022

What is then the result of

<cfdump var="#apiRes#">

 

Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 08, 2022 Aug 08, 2022

Another suggestion for you:

 

<cfset jwtHeader='{"alg":"ES256","kid":"your_KID_key","typ":"JWT"}'>

<cfhttp url="https://api.storekit-sandbox.itunes.apple.com/inApps/v1/history/#transactionid#" method="get" result="apiRes">
    <cfhttpparam type="header" name="JWT" value="#jwtHeader#">
    <cfhttpparam type="header" name="Authorization" value="Bearer #token#">
</cfhttp>

<cfdump var="#apiRes#">

 

Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Engaged ,
Aug 08, 2022 Aug 08, 2022

Apologies for the result ommision.  My frustration is getting the better of me.  Here is the result:

sdsinc_pmascari_0-1659983172781.pngexpand image

 

Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 08, 2022 Aug 08, 2022

Was that the result when you tried both suggestions?

Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Engaged ,
Aug 08, 2022 Aug 08, 2022

Same result.  Every time.  401 Unauthorized

Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 08, 2022 Aug 08, 2022

On further investigation, I see that your original code for evaluating the token is correct. The question I cannot yet answer is whether that is the correct way to create an authorization.

 

If so,

1) Verify whether the URL is correct. 

2) Output the token; does it look like what you expect?

Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 09, 2022 Aug 09, 2022

Paul, in a case like this (where mapping the api doc info to a cf request still fails for a reason not made clear in the error), I'd argue the best next step is to set up postman to make the call. On confirming it works, then modify the cfhttp code to ensure you send EXACTLY what postman sends: down to even every header and value.

 

(And be sure to run postman ON the server/machine that is running cf, as a sanity check. You may find that even postman could get a 401 there, though I'd expect it will not.)

 

Once you have the cfhttp working, you could try taking things away that may not have been needed (various headers, for instance).

 

You may also want to to try using in postman whatever result you got from the cfc. You may find it doesn't work, and that's your problem. (Then you can reconcile what DOES work between that result and whatever original value you created manually using postman, in the first working case.)

 

Yes, it would be nice if somehow it all "just worked". Sometimes it's a lack in the apidocs. Sometimes it's a mistake in what you're trying. And sometimes it's just that you're blazing a trail and are the first to discuss your attempt, so the breadcrumbs you leave are what will help the next person succeed more easily.

 

Sadly there's no doc or example repo showing cf working with the many various APIs. It would be a cool idea, though also a challenge to keep updated, as APIs evolve, old versions get deprecated, etc.

 

Hope the info above may help get you to at least a working example. 


/Charlie (troubleshooter, carehart. org)
Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Engaged ,
Aug 09, 2022 Aug 09, 2022

I tried PostMan.  Same 401 error.

I've been playing with jwt.io

It says the token I'm generating is invalid?  However, when using ES256, the site seems to ask for a Public Key which is not mentioned in Apple's docs.  Nor have I seen any other similar message board posts mention a public key.

 

Anyway...I've seen many suggestions on other boards (in different languages like Python, PHP, Node, etc) and tried them all.  One thing I thought for sure was the answer is the 'exp' and 'iat' in the Payload had to be INTs.  Upon inspection, CF was, indeed, outputing them as strings.  But, when I tried that....  Still no luck. 

So frustrating....

Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 09, 2022 Aug 09, 2022

Ah, well, would you agree then that until you can get it working from postman, there's no reason to bang your head against the wall trying to run it from cf? 🙂

 

Maybe you mean you hoped it might work from cf despite not working from postman...or that the cfc might have helped...or that someone here had done it.

 

Since those are not panning out, and while your searches otherwise have not, you may have better luck asking in still other cfml forums where it may well BE that someone there has already blazed this trail (but just not blogged about it).

 

Try the cfml slack, the Facebook cf programmers group, and even the lucee dev group. I offer links to these in a category of my cf411.com site, specifically https://www.cf411.com/cfhelp.

 

I appreciate that you may have already know of or perhaps even had tried these. I couldn't know. I share this also for others who may find it.

 

I'd look forward to seeing how things turnout. 


/Charlie (troubleshooter, carehart. org)
Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Engaged ,
Aug 09, 2022 Aug 09, 2022

OMG!  I figured it out!

First off - the main issue was the 'exp' and 'iat' values do need to be INT

 

The problem was the jwt.cfc was converting my dates to strings.  So, instead of passing CF dates into the CFC, I converted them to epoch time using an old 'epoch' function I had laying around:

<cfset tokenPayload = {
		"iss":"my-iss-code",
		"iat":int(epochTime(now())),
		"exp":int(epochTime(dateAdd("n",20,now()))),
		"aud":"appstoreconnect-v1",
		"bid":"com.my.app"
	}>

<cffunction name="epochTime" output="No">
	<cfargument name="dt" required="false">

	<cfreturn DateDiff("s", "January 1 1970 00:00", isDefined("arguments.dt") AND isDate(arguments.dt)?arguments.dt:now())>
</cffunction>

 

However, this still didn't work: 401 Unauthorized.

 

But then I noticed something when I tried pasting my token to jwt.io:

sdsinc_pmascari_0-1660061215493.pngexpand image

The time portion of my Epoch code was 4 hours ago!  So, the expiration had long passed!  WTF?

 

SOLUTION:

 

So, I removed my epochtime() function and, instead, made a simple change in the jwt.cfc script using the int() function to make sure the converted time is an INT:

Payload:

<cfset tokenPayload = {
		"iss":"my-iss-code",
		"iat":now(),
		"exp":dateAdd("n",20,now())),
		"aud":"appstoreconnect-v1",
		"bid":"com.my.app"
	}>

Portion of jwt.cfc making sure the converted date is INT:

var duplicatedPayload = duplicate( payload );
        for ( var claim in [ 'iat', 'exp', 'nbf' ] ) {
            if ( duplicatedPayload.keyExists( claim ) && isDate( duplicatedPayload[ claim ] ) ) {
                duplicatedPayload[ claim ] = int(encodingUtils.convertDateToUnixTimestamp( duplicatedPayload[ claim ] ));
            }
        }

 

Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Community Expert ,
Aug 10, 2022 Aug 10, 2022
LATEST

@sdsinc_pmascari , how glad I am to hear of the happy ending. Thanks for sharing.

Translate
Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Resources