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-----
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.
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);
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
GETrequests.- 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With