How to integrate Apple Pay (App Store) server Api with JWT(JWS) authorityοΏΌ

integrate apple pay server api
integrate apple pay server api

Hello guys, Kwam here.

Apple has provided several new server Api recently, according to these Api we can do a lot of things that we couldn’t do before. such as getting an apple order detail by an apple order Id in which feedback by a user or confirm a transaction only with an apple transactionId(sometimes the client app cannot get the payment receipt). also, apple provide Server Notification, compose with these server API, we can build a more reliable payment channel of apple pay.

The official document link is here https://developer.apple.com/documentation/appstoreserverapi.

This blog post will show how to integrate the app store server API to our system step by step. the apple’s document is clear, but a little abstract in some
point, especially how to generate the auth token with JWT standard, and how to validate the response with the JWS standard. Without further ado, let’s dive in.

Step 1: get your config item (issuerId, keyId and private Key)

These three-config items are essential, you can find them on the App Store Connection page.

get your API config

Step 2: generate JWT token and request the API

The API requests authorization based on a token which is set in the header with the “Authorization” key. The token is generated with JWT standard. Before we get the token, we must build the two-part param: jwt header and jwt payload, and then use a jwt implementation library to sign it to get a token string.
Referring to the official document, you can get the jwt header and jwt payload easily. To get the jwt token string, you need a jwt implementation library,
https://jwt.io/introduction gives you lots of choice with different program language. I’m using java and SpringBoot , so my choice is “auth0/java-jwt”, this project can be
find in in GitHub https://github.com/auth0/java-jwt.
First, I need to import the library to my project with maven, edit the pom.xml and add the dependence

<dependency> 
    <groupId>com.auth0</groupId> 
    <artifactId>java-jwt</artifactId>
    <version>4.0.0</version>
</dependency>

Next step is to use it with a simple code to get the token String.

String token = JWT.create().withHeader(YourJwtHeaderMap).withPayload(YourJwtPayloadMap).sign(algorithm);

Obviously, there is an “algorithm” param we must build before, apple use the “ES256 encryption”, in auth0/java-jwt library the algorithm code and create function is “Algorithm.ECDSA256(keyProvider)”. a keyProvider needs to be defined before this call. as we are doing encryption, we just need to override the getPrivateKey() function, but after we request apple’s API, and get the response, we will need to decrypt it, then we need to overwrite the getPublicKeyById() function, I will show it in the next step.

in step 1, we get a private key file with PEM (Privacy Enhanced Mail) format, content of the key file would like this

-----BEGIN PRIVATE KEY-----
some string of the main content
-----END PRIVATE KEY-----

We can just peek at the main content and use it as a key string or just keep the whole content as file. here I choose the string as it does not need to load a file from disk. so, the keyProvider function could be like this:

ECDSAKeyProvider keyProvider = new ECDSAKeyProvider() {
    @Override
    public ECPublicKey getPublicKeyById(String keyId) {
        return null;
    }

    @Override
    public ECPrivateKey getPrivateKey() {
        try {
            byte[] keyBytes = Base64Utils.decode(config.getAppStoreConnectPrivateKey().getBytes());
            KeyFactory kf = KeyFactory.getInstance("EC");
            EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);

            return (ECPrivateKey) kf.generatePrivate(keySpec);
        } catch (NoSuchAlgorithmException |InvalidKeySpecException e) {
            log.error("generatePrivate fail, e:", e);
        }
        return null;
    }

    @Override
    public String getPrivateKeyId() {
        return config.getAppStoreConnectKeyId();
    }
};
Algorithm algorithm = Algorithm.ECDSA256(keyProvider);

If you use the key file format, you can reference the library’s test code, loading key file use a PemUtil.java like this, with more, you need to read the test code.

readPrivateKeyFromFile(PRIVATE_KEY_FILE_256, "EC"));

Right now, we got a token string, fill it to your request header, and send the request, you will get the response, now let’s go to step 3.

Step 3: verify and decode the API response.

I use the “Look Up Order ID” API to demonstrate this step.

After we request the API, we will get the response in Json format

{
    "status": 0,
    "signedTransactions": [
        "xxxx.xxxx.xxxx"
    ]
}

There is a signedTransactions field that needs us to verify and decode, it’s a list with string.
Each string in signedTransactions is encoded with JWS (JSON Web Signature) format, it has three parts.

part one: jws header, a base64 encode string, decode it and get a JWSDecodedHeader , it contains two fields, “alg” and “x5c”, alg field value is the algorithm name, x5c is The X.509 certificate chain that corresponds to the key that the App Store used to secure the JWS.

Part two: jws payload, the main data we need, such as transaction details. a Json format string after base64 encode.

Part three: the Signature of the content.
We need to decode part one, to get the algorithm flag and public key, the x5c field

looks a little complex, as it contains a public key string, an apple certificate, and an apple root certificate. I don’t know how to use the last two parts, temporarily I ignore those certificates, I will use it and add the code to this blog after I figure it out how to deal with those certificates.

Now we have a public key, with it we can verify the signature. the code like this

//get the public key
String publicKey = JWT.decode(response).getHeaderClaim("x5c").asList(String.class).get(0);

ECDSAKeyProvider keyProvider = new ECDSAKeyProvider() {
    @Override
    public ECPublicKey getPublicKeyById(String keyId) {
        try {

            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
            InputStream in = new ByteArrayInputStream(Base64Utils.decode(publicKey.getBytes()));
            X509Certificate certificate = (X509Certificate)certFactory.generateCertificate(in);
            return ((ECPublicKey)certificate.getPublicKey());
        } catch (CertificateException e) {
            log.error("generatePrivate fail, e:", e);
        }
        return null;
    }

    @Override
    public ECPrivateKey getPrivateKey() {
        return null;
    }

    @Override
    public String getPrivateKeyId() {
        return config.getAppStoreConnectKeyId();
    }
};

Algorithm algorithm = Algorithm.ECDSA256(keyProvider);

//verify the response
DecodedJWT r = JWT.require(algorithm).build().verify(response);
//get the business data with Json format
return new String(Base64Utils.decodeFromString(r.getPayload()));

Finally, we finished an App Store Server API request and got the data that we need, enjoy it!

This post is one of my series of posts about apple payment integration ,I will write and share more, if this post helps you or if there are some mistakes, leave a comment.

By Kwam

@KwamWangGoGo

Leave a comment

Your email address will not be published.