Cybersource JWT Authentication with Message-Level Encryption (MLE)

Cybersource REST API authentication has several moving parts: a per-request JWT, a digest of the request body, one or two .p12 keystores, and message-level encryption that wraps the payload in a JWE. The pieces depend on one another, so issues like these can be difficult to troubleshoot.

This guide explains each piece, and the tabs above let you inspect your own artifacts as you go. Everything here is also implemented in a small open-source library, if you prefer to read working code:

The big picture

Every call to the Cybersource REST API carries a freshly signed JWT in the Authorization: Bearer header. The JWT identifies you, because it is signed with your private key, and it carries a digest of the request body so the server can confirm the body was not altered. Message-level encryption then encrypts the body so it is unreadable in transit, and the response comes back encrypted to you.

A request has three parts:

  1. The JWT: your identity and a digest of the body.
  2. The digest: that value, computed a specific way.
  3. MLE: the body encrypted to Cybersource, and the response encrypted back to you.

The JWT

The JWT is signed with RS256 (RSA with SHA-256) using your private key. Two fields in the header matter:

  • alg is RS256.
  • kid is the serialNumber attribute from your signing certificate’s subject. This is not the X.509 serial number, and not your merchant ID. The p12 Explorer tab shows which value this is.

The payload is where Cybersource is specific. A POST request looks like this:

{
  "iat": 1718380800,
  "exp": 1718380920,
  "request-host": "apitest.cybersource.com",
  "request-resource-path": "/tms/v2/tokenize",
  "request-method": "post",
  "iss": "your_merchant_id",
  "jti": "f7c1bec0-189c-472a-afe8-c55b1a019c56",
  "v-c-jwt-version": "2",
  "v-c-merchant-id": "your_merchant_id",
  "digest": "ilXZXw+yriDTJxzXbPdvFvEhsEhNFiXT2lIQ6d9GlYU=",
  "digest-algorithm": "SHA-256"
}

Notes on the claims:

  • exp is two minutes after iat, so a token is valid for two minutes. Clock skew beyond that can cause authentication to fail.
  • request-method is lowercase, and request-resource-path is the path only.
  • digest and digest-algorithm appear only on body methods (POST, PUT, PATCH); GET and DELETE omit them.
  • iss and v-c-merchant-id are your merchant ID, unless you use a meta key (covered below).

Paste a token into the JWT / JWE Decoder tab to see the header and claims, and to check that each required claim is present.

The digest

The digest claim is a SHA-256 hash of the request body, Base64-encoded with standard Base64. Two details account for most digest mismatches:

  1. It covers the exact bytes you send. If you re-serialize the JSON differently from what goes on the wire (key order, whitespace, a trailing newline), the digest will not match.
  2. With MLE, you hash the encrypted envelope, not the plaintext. You encrypt the body first, then compute the digest over the resulting {"encryptedRequest":"..."} JSON, because that is what is transmitted. This is the encrypt-then-digest order.

The Digest Calculator tab computes the SHA-256 for any body, so you can compare it against the digest claim.

The keys in your p12 files

Cybersource provides key material as PKCS#12 (.p12) keystores. There are up to three distinct keys across them:

  • Your signing key and certificate. The private key signs the JWT, and the certificate’s subject serialNumber is your kid. The common name is your merchant ID, or your portfolio ID for a meta key.
  • Cybersource’s MLE certificate, with common name CyberSource_SJC_US. This is the public certificate you encrypt request bodies to. It usually ships inside the request p12.
  • Your response-decryption key, used to decrypt the encrypted response, often in a separate response p12.

You will also see a CA (issuer) certificate in the bundle. That is the authority, not something you sign or encrypt with. The p12 Explorer tab opens a keystore and labels each certificate by common name, serialNumber / kid, validity, and role.

Message-level encryption (MLE)

MLE wraps the body in a JWE (JSON Web Encryption), adding encryption on top of TLS. Cybersource uses:

  • alg is RSA-OAEP-256, which wraps the random content-encryption key to Cybersource’s RSA public key.
  • enc is A256GCM, which encrypts the payload with AES-256-GCM.
  • kid is the serialNumber of the Cybersource certificate you encrypted to.

MLE applies in both directions:

  • Request: you encrypt the body to Cybersource’s public certificate. The wire body becomes {"encryptedRequest":"<jwe>"}, and the digest is taken over that.
  • Response: you add a v-c-response-mle-kid claim naming which of your keys Cybersource should encrypt the response to, then you decrypt it with your response private key.

The Live Test tab runs a sandbox POST /tms/v2/tokenize and shows the full trace: the signed JWT, the request JWE header, the digest, the request headers, and the raw and decrypted response.

Standard key vs. meta key

The two key types differ only in identity:

  • Standard key: iss and the signing certificate’s common name are your merchant ID.
  • Meta key: a portfolio-level key that can act for many merchants. iss and the common name are the portfolio ID, and you still pass the specific v-c-merchant-id on each request.

The claim set, the digest, and MLE are otherwise identical.

The library

The @ryankleindev/cybs-jwt-client library implements all of this and returns a full trace of every artifact: the plaintext body, the signed JWT, the JWE headers, the digest, and the raw and decrypted response.

npm install @ryankleindev/cybs-jwt-client

The full source and usage docs are on GitHub.

Try it

The tabs above are working tools:

  • p12 Explorer: see what is in your keystore and which value is your kid.
  • JWT / JWE Decoder: break down a token and check every required claim.
  • Digest Calculator: confirm your digest matches the bytes you are sending.
  • Live Test: run an end-to-end sandbox tokenize and inspect the trace.

Everything runs against the Cybersource sandbox, and nothing you enter is stored or logged.

Sandbox keys only. Your .p12 and password are sent to the function, used in memory, and never stored or logged. Do not upload production keys.

Open a PKCS#12 (.p12) keystore and list its certificates: common name, the serialNumber that becomes the JWT kid, validity dates, and which cert is yours vs. Cybersource’s MLE cert.

For inspection only. The signature isn’t verified, and nothing is stored or logged. Avoid pasting production tokens you consider sensitive.

Paste a compact JWT (3 segments) or JWE (5 segments). For a JWT, every Cybersource-required claim is checked. The signature is not verified; this is a decoder and validator.

For testing and learning. Nothing you enter is stored or logged.

Compute the SHA-256 base64 digest over the exact request body. This must equal the JWT digest claim. With request MLE on, hash the {"encryptedRequest":…} envelope (the bytes on the wire), not the plaintext.

Sandbox credentials only. Calls the Cybersource sandbox (apitest.cybersource.com) only. Your p12s and passwords are used in memory and never stored or logged. Do not use production keys or live card data.

Run a real sandbox tokenize call (POST /tms/v2/tokenize, request & response MLE on) using your sandbox keys, and see the full trace: the signed JWT, the request JWE header, the digest, request headers, and the raw vs. decrypted response. Powered by the open-source client library.