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-----
Original question (before edit):
401 Unauthorized
: that should mean the authentication token (JWT) you are using is either not properly formatted, not signed correctly, or expired.From your code, you might be missing the "signing the JWT with your private key" step. The JWT must be signed with your private key for the Apple API to authenticate it.
See "Creating API Keys for App Store Connect API" to create the key.
Then, to sign the JWT with said private key:
See "REST Security With JWT Using Java and Spring Security" from Dejan Milosevic.
Also, check that the JWT structure (header, payload, signature) is correct as per Apple's guidelines.
And, as usual, add some error handling to provide more informative feedback in case of failures.
Your updated code now includes:
.p8
file, which is typically used for Apple API authentication. It usesKeyFactory
with "RSA" for generating thePrivateKey
.You now have a
InvalidKeySpecException
indicating an issue with the key specification or format.The
.p8
file likely contains an EC (Elliptic Curve) private key, not RSA. So theKeyFactory
instance should use"EC"
instead of"RSA"
.Make sure the
.p8
file is not corrupted and correctly formatted.And, again, add error handling for loading the private key to pinpoint issues.
For the third edit:
jdk.crypto.ec/sun.security.ec.ECKeyFactory.engineGeneratePrivate(ECKeyFactory.java:168)
should indicate a problem with loading the private key from the.p8
file. The stack trace suggests that the issue lies in thePKCS8EncodedKeySpec
, which is unable to decode the key.You have updated the
KeyFactory
instance to use "EC" instead of "RSA", which is correct for Apple's.p8
private keys. But despite this change, theInvalidKeySpecException
persists, indicating the problem is not with the choice of key factory algorithm (RSA or EC), but rather with the format or content of the key file.The error message "
Unable to decode key
" and "extra data at the end
" suggest that the.p8
file might not be in the correct PKCS#8 format.So try and make sure the
.p8
file is a proper PKCS#8 format private key. It should start with "-----BEGIN PRIVATE KEY-----
" and end with "-----END PRIVATE KEY-----
". Any additional data or formatting issues can cause the error.If the
.p8
file contains any headers or footers (like "BEGIN PRIVATE KEY
"), these need to be removed before being passed toPKCS8EncodedKeySpec
.The key data in the
.p8
file is typically Base64 encoded. Make sure it is correctly decoded to its binary form before using it inPKCS8EncodedKeySpec
.With some error handling:
If you still have an issue with loading the private key, even after formatting and Base64 decoding the content, that would suggest an issue with the key itself or the way it is being processed.
I will assume your
.p8
file does starts with-----BEGIN PRIVATE KEY-----
and ends with-----END PRIVATE KEY-----
, and that it is complete and not truncated. You have generated a new key from the Apple Developer account, so you can compare with your previous one.Make sure the Base64 decoding is happening correctly. Incorrect decoding can lead to an invalid key specification.
And make sure the file reading process is not altering the key content in any way. For example, make sure the character encoding used to read the file matches the encoding of the file.
This is basically your code, but in a function set to throw exception in case of error, with a replaceAll using
\s+
instead of\s+
, and with some logging (theSystem.out.println
)Or, for testing, try a more direct approach to decoding:
In any case, try using a tool like OpenSSL to check the validity of the key. That can help determine if the issue lies with the key itself or the Java code.
That command should output the key details if the file is correctly formatted. If it fails, the problem likely is with the key file itself.