1

I'm implementing Apple In-App Purchase server-to-server notifications in Laravel. I have set up the necessary credentials (issuer_id, key_id, and p8 private key) from App Store Connect and configured them in my application. However, I'm unable to decode the signedPayload received from Apple's notifications.

Here’s my implementation:

class ServerNotificationAppleController extends Controller
{
    private $storeKitKeysUrl = 'https://appleid.apple.com/auth/keys';

    public function handleNotification(Request $request)
    {
        Log::info('Apple Notification Request:', $request->all());

        $signedPayload = $request->input('signedPayload');

        if (!$signedPayload) {
            return response()->json(['error' => 'signedPayload not provided'], 400);
        }

        $jwtToken = $this->generateAppleJWT();

        $response = Http::withHeaders([
            'Authorization' => 'Bearer ' . $jwtToken,
        ])->get($this->storeKitKeysUrl);

        Log::info('Apple Keys Status:', ['status' => $response->status()]);
        Log::info('Apple Keys Body:', ['body' => $response->body()]);

        if ($response->status() !== 200) {
            return response()->json(['error' => "Apple public keys couldn't be retrieved"], 401);
        }

        $keysData = $response->json();

        $validatedPayload = $this->validateSignedPayload($signedPayload, $keysData);

        if (!$validatedPayload) {
            return response()->json(['error' => 'Invalid signedPayload'], 400);
        }

        Log::info("Apple Purchase Data:", (array)$validatedPayload);

        return response()->json(['message' => 'Notification processed successfully'], 200);
    }

    private function generateAppleJWT()
    {
        $keyId = config('services.apple.key_id');
        $issuerId = config('services.apple.issuer_id');
        $privateKey = file_get_contents(storage_path(config('services.apple.private_key')));

        $nowUtc = Carbon::now();
        $expirationUtc = $nowUtc->copy()->addMinutes(20);

        $payload = [
            'iss' => $issuerId,
            'iat' => $nowUtc->timestamp,
            'exp' => $expirationUtc->timestamp,
            'aud' => 'appstoreconnect-v1',
        ];

        $header = [
            'kid' => $keyId,
            'alg' => 'ES256',
            'typ' => 'JWT'
        ];

        return JWT::encode($payload, $privateKey, 'ES256', $keyId, $header);
    }

    private function validateSignedPayload($signedPayload, $keysData)
    {
        try {
            $jwkKeys = JWK::parseKeySet($keysData);
            $allowedAlgs = new \stdClass();
            $allowedAlgs->algos = ['ES256']; // Using ES256
            return JWT::decode($signedPayload, $jwkKeys, $allowedAlgs);
        } catch (\Exception $e) {
            Log::error("Apple Purchase Validation Error: " . $e->getMessage() . " Trace: " . $e->getTraceAsString());
            return null;
        }
    }
}

The problem:

signedPayload is received correctly. Fetching Apple's public keys works fine (status 200). validateSignedPayload() fails to decode the payload and logs an error.

Questions:

  • Am I correctly fetching and using Apple's public keys for validation?
  • Do I need to extract a specific part of signedPayload before decoding?
  • Could the issue be related to how I'm parsing the JWK keys?

Any insights or corrections would be appreciated!

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.