EBjames
EBjames

Reputation: 41

Unable to connect to Apple App Store API with JWT token

I need to generate JWT token for Store Connect API. This is my token generating code:

console.log("🏃 appStoreConnectAPIFromNode.js running 🏃‍")

const fs   = require('fs');
const jwt  = require('jsonwebtoken'); // npm i jsonwebtoken
// You get privateKey, apiKeyId and issuerId from your Apple App Store Connect account
const privateKey = fs.readFileSync("./AuthKey-XXXXXXXX.p8") // this is the file you can only download once and should treat like a real, very precious key.
const apiKeyId = "XXXXXXXX"
const issuerId = "XXXXXXXX-XXXX-XXXX-XXX-XXXXXXXXXX"
let now = Math.round((new Date()).getTime() / 1000); // Notice the /1000 
let nowPlus20 = now + 1199 // 1200 === 20 minutes

let payload = {
    "iss": issuerId,
    "exp": nowPlus20,
    "aud": "appstoreconnect-v1",
    "iat": now
}

let signOptions = {
    "algorithm": "ES256", // you must use this algorythm, not jsonwebtoken's default
    header : {
        "alg": "ES256",
        "kid": apiKeyId,
        "typ": "JWT"
    }
};

let token = jwt.sign(payload, privateKey, signOptions);
console.log('@token: ', token);

fs.writeFile('Output.txt', token, (err) => { 
      
    // In case of a error throw err. 
    if (err) throw err; 
})

I'm getting this response though

 "errors": [{
                "status": "401",
                "code": "NOT_AUTHORIZED",
                "title": "Authentication credentials are missing or invalid.",
                "detail": "Provide a properly configured and signed bearer token, and make sure that it has not expired. Learn more about Generating Tokens for API Requests https://developer.apple.com/go/?id=api-generating-tokens"
        }]

I thing the problem is with token(directly with signature). When I try decode token on https://jwt.io/#debugger-io, my payload and header is decoded properly. Status: Invalid Signature

What I do wrong? Any ideas how do it properly?

Upvotes: 4

Views: 2841

Answers (5)

Pulasthi Aberathne
Pulasthi Aberathne

Reputation: 428

You can use simple sign option for this.

const PRIVATE_KEY = fs.readFileSync(p8FilePath, "utf8");
const ISSUER_ID = "ID";
const KEY_ID = "Key_ID";

 let payload = {
      iss: ISSUER_ID,
      exp: NOW_PLUS_20,
      aud: "appstoreconnect-v1",
    };

    let signOptions = {
      algorithm: "ES256",
      header: {
        alg: "ES256",
        kid: KEY_ID,
        typ: "JWT",
      },
    };

    let token = jwt.sign(payload, PRIVATE_KEY, signOptions);

You can use this token in your API call

Upvotes: 2

Christopher Thomas
Christopher Thomas

Reputation: 4723

I recently needed to solve this problem too and after following a lot of tutorials which often gave the wrong answer for the app store connect api. I found a way to make it work. This is how I did it in Java.

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.interfaces.ECPrivateKey;
import java.security.KeyFactory;
import java.io.FileReader;
import java.util.Base64;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

// The code is like this so try it in whatever method you want, 
// break it up into pieces so it's a bit more readable

// These variables are the minimum you should need
String keyFile = "/path/to/app_store_AuthKey_XXXX.p8";
// The XXXX is the same XXXX from the keyFile filename itself
String keyId = "XXXX";
String issuerId = "1234-1234-1234-1234-1234";

FileReader kfReader = new FileReader(keyFile);
PEMParser pemParser = new PEMParser(kfReader);
PrivateKeyInfo info = (PrivateKeyInfo) pemParser.readObject();
byte[] pkData = info.getEncoded();

// Convert the PrivateKeyInfo object to a PKCS8EncodedKeySpec object
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkData);

KeyFactory keyFactory = KeyFactory.getInstance("EC");
ECPrivateKey privateKey = (ECPrivateKey) keyFactory.generatePrivate(keySpec);

Algorithm algorithm = Algorithm.ECDSA256(null, privateKey);

Instant expiration = Instant.now().plus(TIMEOUT_MINUTES, ChronoUnit.MINUTES);

return JWT.create()
          .withKeyId(keyId)
          .withIssuer(issuerId)
          .withExpiresAt(expiration)
          .withAudience("appstoreconnect-v1")
          .sign(algorithm);

Upvotes: 1

Gandhi
Gandhi

Reputation: 41

I'm also facing this and me and teammate solved it.....

solution:

  1. remove "iat" in payload(this is important): like this,
let payload = {
     iss: *******,
     exp: *******,
     aud: "appstoreconnect-v1",
     bid: "your_bundle_id",
   };
  1. url : i used axios :
const { data } = await axios({
     method: "GET",
     url: "https://api.storekit.itunes.apple.com/inApps/v1/history/",
     headers: {
       "Content-type": "application/json",
       Authorization: `Bearer ${token}`,
     },
   });
res.json(data);

Upvotes: 4

Jimmy Bosse
Jimmy Bosse

Reputation: 1404

I was trying to resolve my own issues dealing with making a JWT to connect to the Apple Music API and there were two things I noticed in your payload. The first, that you are adding the 1200 to the iat, fixed my issued, so thank you. The second is the function for now is returning a float. I think the API would like an int. I would try "iat": parseInt(now) or now = Math.floor(new Date().getTime() / 1000)

Upvotes: 0

Ahmed Abbas
Ahmed Abbas

Reputation: 7

According to jsonwebtoken usage instructions. You can use the options directly on an empty payload as follows:

let signOptions = {
    issuer: issuerId,
    keyid: apiKeyId,
    expiresIn: '20m',
    audience: 'appstoreconnect-v1',
    algorithm: 'ES256'
};

let token = jwt.sign({}, privateKey, signOptions);

Upvotes: -2

Related Questions