Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generating Tokens for Apple API Requests [closed]

I create this class to access Apple API Requests

@Transactional(readOnly = true)
public class AppleAPIService {

    public static void main(String[] args) {


            Path privateKeyPath = Paths.get("/Users/ricardolle/IdeaProjects/mystic-planets-api/src/main/resources/cert/AuthKey_5425KFDYSC.p8");

    String keyContent = new String(Files.readAllBytes(privateKeyPath), StandardCharsets.UTF_8);
    System.out.println("Original Key Content: " + keyContent); // Logging the original content
    keyContent = keyContent.replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replaceAll("\\s+", ""); // Remove all whitespaces and newlines, more robust than just replacing \n
    System.out.println("Processed Key Content: " + keyContent); // Logging processed content
    byte[] decodedKey = Base64.getDecoder().decode(keyContent);
    PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decodedKey);
    KeyFactory kf = KeyFactory.getInstance("EC");
    PrivateKey pk =  kf.generatePrivate(spec);

    Map<String, Object> headerMap = new HashMap<>();

    headerMap.put("alg", "ES256"); // Algorithm, e.g., RS256 for asymmetric signing
    headerMap.put("kid", "5425KFDYSC"); // Algorithm, e.g., RS256 for asymmetric signing
    headerMap.put("typ", "JWT"); //
    
    String issuer = "68a6Se82-111e-47e3-e053-5b8c7c11a4d1"; // Replace with your issuer
    //String subject = "subject"; // Replace with your subject
    long nowMillis = System.currentTimeMillis();
    Date issuedAt = new Date(nowMillis);
    Date expiration = new Date(nowMillis + 3600000); // Expiration time (1 hour in this example)


    JwtBuilder jwtBuilder = Jwts.builder()
            .setHeader(headerMap)
            .setIssuer(issuer)
            .setAudience("appstoreconnect-v1")
            .setIssuedAt(issuedAt)
            .signWith(pk)
            .setExpiration(expiration);
    
    // Print the JWT header as a JSON string
    String headerJson = jwtBuilder.compact();

    System.out.println("JWT Header: " + headerJson);

    String apiUrl = "https://api.appstoreconnect.apple.com/v1/apps";

    // Create headers with Authorization
    HttpHeaders headers = new HttpHeaders();
    headers.set("Authorization", "Bearer " + headerJson);
    headers.setContentType(MediaType.APPLICATION_JSON);

    // Create HttpEntity with headers
    HttpEntity<String> entity = new HttpEntity<>(headers);

    // Make GET request using RestTemplate
    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<String> response = restTemplate.exchange(
            apiUrl,
            HttpMethod.GET,
            entity,
            String.class
    );

    // Handle the response
    if (response.getStatusCode() == HttpStatus.OK) {
        String responseBody = response.getBody();
        System.out.println("Response: " + responseBody);
    } else {
        System.out.println("Error: " + response.getStatusCodeValue());
    }

    // Print the JWT payload as a JSON string
    String payloadJson = jwtBuilder.compact();
    System.out.println("JWT Payload: " + payloadJson);

but I have this error:

Exception in thread "main" org.springframework.web.client.HttpClientErrorException$Unauthorized: 401 Unauthorized: "{<EOL>?"errors": [{<EOL>??"status": "401",<EOL>??"code": "NOT_AUTHORIZED",<EOL>??"title": "Authentication credentials are missing or invalid.",<EOL>??"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"<EOL>?}]<EOL>}"
at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:106)
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:183)
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:137)
at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:932)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:881)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:781)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:663)
at com.mysticriver.service.AppleAPIService.main(AppleAPIService.java:77)

Opening the file with an editor gives me this:

-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg5Fu6zyvQDhgGvevK
pe4OYs32cFSz1oxLd/YCYWJSOPagCgYIKoZIzj0DAQehRANCAATrJf+q7/nieM4y
V9/v71e/Xl/aS+LF4riW5lkcld8lFQB5ekivp5T7w57t6nqp8rCqtq79nEhIyzDr
hCMnmLEk
-----END PRIVATE KEY-----
like image 805
Nunyet de Can Calçada Avatar asked Dec 28 '25 06:12

Nunyet de Can Calçada


2 Answers

Easiest solution is to use BountyCastle Library.

This library will takes care of everything when it comes to removal of the unnecessary headers and decoding the Base64 PEM data.

Note: BountyCastle has a good support for Elliptic Curve Cryptography Algorithm parsing.

Also, you try to use com.auth0:java-jwt dependency instead as it provides much more functionalities than io.jsonwebtoken:jjwt dependency.

Add this dependency in the pom.xml :

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk18on</artifactId>
    <version>1.76</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.4.0</version>
</dependency>

Change/Update your logic to this:

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;

import java.io.FileReader;
import java.security.KeyFactory;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Transactional(readOnly = true)
public class AppleAPIService {

    public static void main(String[] args) {

        try (PemReader pemReader = new PemReader(
                new FileReader("/Users/ricardolle/IdeaProjects/mystic-planets-api/src/main/resources/cert/AuthKey_5425KFDYSC.p8"))) {

            KeyFactory keyFactory = KeyFactory.getInstance("EC");
            PemObject pemObj = pemReader.readPemObject();
            byte[] content = pemObj.getContent();
            PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(content);
            ECPrivateKey privateKey = (ECPrivateKey) keyFactory.generatePrivate(privateKeySpec);

            String token = JWT.create()
                    .withKeyId("5525KFDYSC")
                    .withIssuer("69a6de82-121e-48e3-e053-5b8c7c11a4d1")
                    .withExpiresAt(new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)))
                    .withClaim("scope", Collections.singletonList("GET /v1/apps"))
                    .withAudience("appstoreconnect-v1")
                    .sign(Algorithm.ECDSA256(privateKey));

            System.out.println("JWT token: " + token);


            // Create headers with Authorization
            HttpHeaders headers = new HttpHeaders();
            headers.setBearerAuth(token);
            headers.setContentType(MediaType.APPLICATION_JSON);

            // Create HttpEntity with headers
            HttpEntity<String> entity = new HttpEntity<>(headers);

            // Make GET request using RestTemplate
            ResponseEntity<String> response = new RestTemplate().exchange(
                    "https://api.appstoreconnect.apple.com/v1/apps",
                    HttpMethod.GET, entity, String.class);

            // Handle the response
            if (response.getStatusCode() == HttpStatus.OK) {
                String responseBody = response.getBody();
                System.out.println("Response: " + responseBody);
            } else {
                System.out.println("Error: " + response.getStatusCodeValue());
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

}

Please look at this Apple Developer thread forum question to resolve your issue - https://developer.apple.com/forums/thread/707220

If the above thread is still not resolving your issue, then try adding bid (apple bundle id) via withClaims("bid", "put your bundle id")

That's all.

like image 180
Anish B. Avatar answered Dec 30 '25 22:12

Anish B.


tl;dr

Try providing an expiration no greater than 20 minutes, let's say, 15, for instance (although the documentation states no greater than I am afraid it should be less than 20):

Date expiration = new Date(nowMillis + 15 * 60 * 1000);

Details

The last version of the code provided in your answer is mostly fine.

I think the problem has to do with the lifetime of the token you are specifying, one hour.

As explained in the Apple Developer documentation when describing the expiration JWT payload field:

The token’s expiration time in Unix epoch time. Tokens that expire more than 20 minutes into the future are not valid except for resources listed in Determine the Appropriate Token Lifetime.

The referenced Determine the Appropriate Token Lifetime documentation states that the App Store Connect accepts a token with a lifetime greater than 20 minutes if:

  • The token defines a scope.
  • The scope only includes GET requests.
  • The resources in the scope allow long-lived tokens.

Your Java code meets the first two conditions but not the third one: the aforementioned documentation lists the resources that can accept long-lived tokens and the List Apps endpoint you are using, in general, the Apps resource, is not included in it.

As indicated, to solve the problem, please, try defining an expiration less than 20 minutes when performing your request. For example:

Date expiration = new Date(nowMillis + 15 * 60 * 1000);

The rest of your code looks fine: please, only, be aware that all the information you provided when generating your JWT token is the right one, and that the key is not revoked and it has been assigned a role authorized to perform the request.

Please, consider for reference this related article, I think it exemplifies very well how to perform the operation.

like image 33
jccampanero Avatar answered Dec 30 '25 22:12

jccampanero