---
page_title: JWT (JWE + JWS) Guide
product: API Reference
page_source: https://juspay.io/in/docs/api-reference/docs/express-checkout/jwt-jwe--jws-guide
llms_txt: https://juspay.io/in/docs/llms.txt
product_llms_txt: https://juspay.io/in/docs/api-reference/llms.txt
---


# JWT (Encryption & Signing) Guide



To ensure secure API communication, Juspay uses the **JOSE**  framework, primarily through **JWS**  (JSON Web Signature) for digitally signing/verifying messages and **JWE**  (JSON Web Encryption) for encrypting/decrypting them. This dual-layer security helps prevent fraud, protect sensitive transaction data, and verify the sender’s identity.


### In Depth Analogical Understanding of the crypto setup



## Analogical Understanding of crypto Setup



Let’s say we want to have a two way secure messaging system that ensures integrity (no tempering) and confidentiality (no visibility). To facilitate this we use a briefcase with two locks with two keys individually. The two keys are public and private keys (yes the same RSA keys). Public keys can be shared and private keys have to be kept private. The properties of two lock:

1. **Integrity Lock (JWS)** :
   
   * Ensures the contents haven’t been tampered with. But you can see the contents of the briefcase. Think of it as seal on a document, if malformed the document is tampered with and must not be used.
   * You can lock (sign) this by private key and unlock (verify) this by public key. They key usage is intuitive as you don’t want anyone to replace your signature
2. **Confidentiality Lock (JWE)** : 
   
   * Ensures only the recipient of the briefcase can see the contents. You cannot even see the contents of the briefcase.
   * You can lock (encrypt) this by public key and unlock (decrypt) this by private key. The key usage intuitive as you want only a trusted person to read the contents.

Using the combination of both the locks we have ensured integrity and confidentiality of the secret. Now let’s see how we send (http request) and receive (http response) the message securely by taking the example of Merchant and Juspay.


### Setup:



For this to work both parties will generate the keys and mutually share the**public keys**  with each other (This happens in Juspay via dashboard check the respective section). Now both parties will use **their private key**  and **others public key**  for the secure process.


### Send the secret message:



1. **Merchant Sends:-**  The merchant sends the request by first applying the integrity lock (**JWS**  Sign) using **their own private key** . Once signed, the payload is then secured with the confidentiality lock (JWE Encrypt) using Juspay’s public key.
2. **Juspay Reads:** - Juspay receives the message, then decrypts the **JWE**  (confidentiality layer) using **its private key**  and verifies the **JWS**  (integrity layer) using the **merchant’s public key** .


### Receive the secret message:



1. **Juspay Sends** :- Juspay sends the request by first acquiring integrity lock (**JWS**  Sign) by **Juspay’s own private key.**  Once it's done, the secret is then locked by Confidentiality lock (**JWE**  Encrypt) by **Merchant’s public key** .
2. **Merchant Reads:-** Merchant receives the above message then unlock (**JWE**  Decrypt) the Confidentiality lock by **Merchant’s private key**  and unlock (**JWS** Verify) the Integrity lock by **Juspay’s public key.**


### Special Mention: Key UUID:



Key UUID is used to ask Juspay to use a particular set of keys as merchants can general any number of keys.




## JWT Setup



JWTs use 2048-bit asymmetric key pairs—one public and one private key. Since JWT involves both signing and encryption, **two separate key pairs**  are used:

* **Merchant’s Key Pair** : Generated by the merchant. The public key is shared with Juspay. Juspay uses merchant’s public key to verify signed requests and to encrypt responses sent back to the merchant.
* **Juspay’s Key Pair** : Generated by Juspay and the public key shared. Merchant should use Juspay’s public key to encrypt the requests and to verify the responses send back to the merchant.


### Keys Generation Substep


How to Generate 2 sets of Public Private Key required for JWT Authentication:

1. Login to Juspay Portal (Sandbox: [https://sandbox.portal.juspay.in](https://sandbox.portal.juspay.in); Production: [https://portal.juspay.in](https://portal.juspay.in)[)](https://portal.juspay.in)2)
2. Navigate to Payments -> Settings -> Security module
3. Scroll Down to JWT Keys section
4. Click on Upload New JWT
5. If merchants do not have a public-private key pair generated,click on option 1 - **I don’t have the JWT Keys, I want to auto generate the keys** .
   
   1. Juspay public key and merchant’s private key will be auto downloaded as a zip file in the merchant's default browser download folder.
   2. **Juspay will never store a merchant's private key even though the keys are auto generated.**
   
   [Video](https://dth95m2xtyv8v.cloudfront.net/tesseract/assets/api-reference/Screen%20Recording%202024-12-23%20at%2011.33.12%E2%80%AFAM.mov)
6. If a merchant has already generated or wants to generate a new set of public-private keys, they can click on option 2 - **I have the JWT Keys already, I want to manually upload the Public Key.** 
   
   1. Generate a new pair of RSA 2048 bit keys, using commands below (_**Skip this step if you already have keys**_ ):
      
      
      #### GenerateRSAKeys Code Snippet:
      
      ```generatersakeys
      openssl genrsa -out private-key.pem 2048
      openssl rsa -in private-key.pem -pubout -out public-key.pem
      ```
   2. Merchants will have to upload their public key in the upload file section.
   3. Post upload, Juspay’s public Key will be auto downloaded in the merchant's default browser download folder.
   
   [Video](https://dth95m2xtyv8v.cloudfront.net/tesseract/assets/api-reference/Screen%20Recording%202024-12-20%20at%2012.58.56%E2%80%AFPM-CDYaR.mov)
7. At the end of this step, merchants will have 2 keys - **Merchant’s private key**  and **Juspay’s public key**  which will be used for signing and encryption.
8. Kindly keep a note of **Key Uuid**  for the JWT keys to be used for encryption and signing. This will be displayed on dashboard post generation.




## Sample Codes



To run the sample codes please ensure dependencies and files are properly setup. Encryption and Decryption codes make use of KeyProvider class or File to read keys.

> **Note**
> Only for demo purposes the keys have been hardcoded inside the code, it’s unsafe and unsecured. Please keep the keys safe either in file system, env variables or some HSM.




### Dependencies



Some of the programming languages use external dependencies. Please inject these dependencies in your code accordingly:

> **Note**
> These libraries do not belong to Juspay, hence merchants discretion is required.



1. **Java** : [nimbus-jose-jwt](https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt), [bouncy-castle](https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on)
2. **PHP** : [web-token/jwt-framework](https://packagist.org/packages/web-token/jwt-framework). Please note that some additional dependencies may be required to build this if you are using version 4 or upwards of this dependency, which are listed below:
   
   * [web-token/jwt-signature-algorithm-rsa](https://packagist.org/packages/web-token/jwt-signature-algorithm-rsa)
   * [web-token/jwt-encryption-algorithm-rsa](https://packagist.org/packages/web-token/jwt-signature-algorithm-rsa?query=web-token%252Fjwt-encryption-algorithm-rsa)
   * [web-token/jwt-encryption-algorithm-aesgcm](https://packagist.org/packages/web-token/jwt-encryption-algorithm-aesgcm)
3. **C#** : [jose-jwt](https://www.nuget.org/packages/jose-jwt/)
4. **Python** : [pycryptodome](https://pypi.org/project/pycryptodome/)


### Key Provider




#### NodeJS Code Snippet:

```nodejs
const crypto = require("crypto");

exports.getKeyUUID = () => {
  return "key_XXXXXXXXXXXXXX"; // add Key ID Here
};

/**
 * Reads a public key from a string.
 * @param {string} [keyString] - Public key string in PEM format.
 * @returns {crypto.KeyObject} Public key object.
 * @throws {Error} If the key string is undefined or invalid.
 */
exports.readJuspayPublicKey = () => {
  const keyString = `-----BEGIN PUBLIC KEY-----...Your Juspay Public Key here...-----END PUBLIC KEY-----`; // Store it in file or another secure locations, this is just sample
  return crypto.createPublicKey({
    key: keyString,
    format: "pem",
  });
}

/**
 * Reads a private key from a string.
 * @returns {crypto.KeyObject} Private key object.
 * @throws {Error} If the key string is undefined or invalid.
 */
exports.readMerchantPrivateKey = () => {
  const keyString = `-----BEGIN RSA PRIVATE KEY-----*********-----END RSA PRIVATE KEY-----`; // Store it in file or another secure locations, this is just sample
  return crypto.createPrivateKey({
    key: keyString,
    format: "pem",
  });
}

```

#### Python Code Snippet:

```python
from Crypto.PublicKey import RSA

def get_key_uuid() -> str:
    return "key_XXXXXXXXXXXXXX" # add Key ID Here


def read_juspay_public_key() -> RSA.RsaKey:
    key_string = """-----BEGIN PUBLIC KEY-----********************-----END PUBLIC KEY-----""" # Store it in file or another secure locations, this is just sample
    return RSA.import_key(key_string)


def read_merchant_private_key() -> RSA.RsaKey:
    key_string = """-----BEGIN RSA PRIVATE KEY-----*********-----END RSA PRIVATE KEY-----""" # Store it in file or another secure locations, this is just sample
    return RSA.import_key(key_string)

```

#### Java Code Snippet:

```java
import java.security.*;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton;

public class KeyProvider {
    static {
        /**
         * Adds BouncyCastle provider if missing.
         * Required only for PKCS#1 key support or BC-specific features.
         * PKCS#8 keys are supported natively by Java and do not need this.
         */
        if (Security.getProvider("BC") == null) {
            Security.addProvider(BouncyCastleProviderSingleton.getInstance());
        }
    }

    /**
     * Returns the Key ID used in the JWS and JWE headers.
     * <p>
     * This is used to help identify the key in use and may be rotated periodically.
     *
     * @return A hardcoded Key ID string.
     */
    public static String getKeyUUID() {
        return "key_XXXXXXXXXXXXXX"; // add Key ID Here
    }

    /**
     * Reads Juspay's RSA public key from a hardcoded PEM-formatted string.
     * <p>
     * In production, the key should be loaded securely from a file or secure storage.
     *
     * @return Juspay's RSA public key object.
     * @throws Exception if the key string is invalid or cannot be parsed.
     */
    public static RSAPublicKey readJuspayPublicKey() throws Exception {
        String keyString = "-----BEGIN PUBLIC KEY-----********************-----END PUBLIC KEY-----"; // Store it in file or another secure locations, this is just sample
        String normalizedPem = keyString
                .replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "")
                .replaceAll("\\s", "");  // Remove all whitespace/newlines
        byte[] keyBytes = Base64.getDecoder().decode(normalizedPem);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return (RSAPublicKey) keyFactory.generatePublic(keySpec);
    }

    /**
     * Reads the merchant's RSA private key from a PEM-formatted string.
     * <p>
     * Supports both PKCS#1 and PKCS#8 formats. In production, store the key securely.
     *
     * @return The merchant's RSA private key object.
     * @throws Exception if the key string is invalid or cannot be parsed.
     */
    public static PrivateKey readMerchantPrivateKey() throws Exception {
        String pem = "-----BEGIN RSA PRIVATE KEY-----*********-----END RSA PRIVATE KEY-----"; // Store it in file or another secure locations, this is just sample

        pem = pem.replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replace("-----BEGIN RSA PRIVATE KEY-----", "")
                .replace("-----END RSA PRIVATE KEY-----", "")
                .replaceAll("\\s", "");
        byte[] pkcs8Bytes = Base64.getDecoder().decode(pem);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(pkcs8Bytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePrivate(keySpec);
    }

    public static void main(String[] args) throws Exception {
        PrivateKey ignored = readMerchantPrivateKey();
        RSAPublicKey ignored2 = readJuspayPublicKey();
    }
}
```

#### PHP Code Snippet:

```php
<?php

require_once 'vendor/autoload.php';

use Jose\Component\Core\JWK;
use Jose\Component\KeyManagement\JWKFactory;

class KeyProvider
{
    public static function getKeyUUID(): string
    {
        return 'key_XXXXXXXXXXXXXX'; // add Key ID Here
    }

    public static function readJuspayPublicKey(): JWK
    {
        $juspayPublicKeyPEM = <<<PEM
-----BEGIN PUBLIC KEY-----
...Your Juspay Public Key here...
-----END PUBLIC KEY-----
PEM;
        return JWKFactory::createFromKey($juspayPublicKeyPEM);
    }

    public static function readMerchantPrivateKey(): JWK
    {
        $merchantPrivateKeyPEM = <<<PEM
-----BEGIN PRIVATE KEY-----
...Your Merchant Private Key here...
-----END PRIVATE KEY-----
PEM;
        return JWKFactory::createFromKey($merchantPrivateKeyPEM);
    }
}

```

#### C# Code Snippet:

```c#
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.IO;

public class KeyProvider
{
    public static String getKeyUUID()
    {
        return "key_XXXXXXXXXXXXXX"; // add Key ID Here
    }

    public static RSA readJuspayPublicKey()
    {
        string publicKeyPem = @"
-----BEGIN PUBLIC KEY-----
...Your Juspay Public Key here...
-----END PUBLIC KEY-----"; // Store it in file or another secure locations, this is just sample

        RSA rsa = RSA.Create();
        rsa.ImportFromPem(publicKeyPem.ToCharArray());

        return rsa;
    }

    public static RSA readMerchantPrivateKey()
    {
        string keyString = @"
-----BEGIN RSA PRIVATE KEY-----
...Your Merchant Private Key here...
-----END RSA PRIVATE KEY-----"; // Store it in file or another secure locations, this is just sample

        RSA rsa = RSA.Create();
        rsa.ImportFromPem(keyString.ToCharArray());

        return rsa;
    }
}

```



### JWT Encryption



Pseudo Code


#### Shell Code Snippet:

```shell
Merchant:
   [Payload]
      ↓ stringify the request and make JWS Headers {alg=RS256, kid=key_uuid}
      ↓ sign with Merchant's private key
   [JWS Token]
      ↓ make JWE Headers {enc=RSA-256-OAEP, alg=A256GCM, kid=key_uuid, cty=JWT}
      ↓ encrypt raw JWS Token with Juspay's public key
   [JWE Token]
      ↓ make request payload {"JWT": "JWE Token"}
   [Request Body] → Sent to Juspay

Juspay:
   ↓ Fetch the correspoding keys using key_uuid (headers are just base64encoded)
   ↓ decrypt with Juspay's private key
   ↓ verify signature using Merchant’s public key
   → Trusted, confidential data

```


Demo Encryption Code


#### Nodejs Code Snippet:

```nodejs
const crypto = require("crypto");
const fs = require("fs");
const { readMerchantPrivateKey, readJuspayPublicKey, getKeyUUID } = require("./keyprovider");

(async () => {
  const claims =
    '{ "order_id" : "test_2340", "amount": 1}'; // add Stringify JSON Request Here
  const keyId = getKeyUUID();
  const publicKey = readJuspayPublicKey();
  const privateKey = readMerchantPrivateKey();

  const encryptedPayload = jwtEncrypt(claims, keyId, publicKey, privateKey);
  const serializedApiRequest = {
    JWT: encryptedPayload,
  };
  console.log(JSON.stringify(serializedApiRequest));
})();

//  ENCRYPT
/**
 * Encrypts the given data.
 * @param {string} data - Data to encrypt.
 * @param {string} keyId - Key ID.
 * @param {crypto.KeyObject} publicKey - Public key.
 * @returns {string} Encrypted token.
 * @throws {Error} If encryption fails.
 */
function encrypt(data, keyId, publicKey) {
  const headers = {
    alg: "RSA-OAEP",
    enc: "A256GCM",
    cty: "JWT",
    kid: keyId,
  };
  const aad = encodeBase64Url(JSON.stringify(headers));
  const cek = crypto.randomBytes(32);
  const cekOptions = {
    key: publicKey,
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
  };
  const encryptedKey = encodeBase64UrlFromBuffer(
    crypto.publicEncrypt(cekOptions, cek)
  );
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", cek, iv);
  cipher.setAutoPadding(false);
  cipher.setAAD(Buffer.from(aad));
  const cipherOutput = Buffer.concat([cipher.update(data), cipher.final()]);
  const authTag = cipher.getAuthTag();
  const ivText = encodeBase64UrlFromBuffer(iv);
  const cipherText = encodeBase64UrlFromBuffer(cipherOutput);
  const tag = encodeBase64UrlFromBuffer(authTag);
  return `${aad}.${encryptedKey}.${ivText}.${cipherText}.${tag}`;
}

/**
 * Signs the given claims.
 * @param {string} claims - Claims to sign.
 * @param {string} keyId - Key ID.
 * @param {crypto.KeyObject} privateKey - Private key.
 * @returns {string} Signed string.
 * @throws {Error} If signing fails.
 */
function sign(claims, keyId, privateKey) {
  const signer = crypto.createSign("RSA-SHA256");
  const signatureHeader = `{"alg":"RS256","kid":"${keyId}"}`;
  const header = encodeBase64Url(signatureHeader);
  const payload = encodeBase64Url(claims);
  const data = `${header}.${payload}`;
  signer.update(data);
  const signedBuffer = signer.sign(privateKey);
  const signed = encodeBase64UrlFromBuffer(signedBuffer);
  return `${header}.${payload}.${signed}`;
}

/**
 * Encrypts the given data and returns a jwe token.
 * @param {string} data - Data to encrypt.
 * @param {string} keyId - Key ID.
 * @param {crypto.KeyObject} publicKey - Public key.
 * @param {crypto.KeyObject} privateKey - Private key.
 * @returns {string} Encrypted token.
 */
function jwtEncrypt(data, keyId, publicKey, privateKey) {
  const signed = sign(data, keyId, privateKey);
  return encrypt(signed, keyId, publicKey);
}

// UTILS
/**
 * Encodes a string into Base64 URL format.
 * @param {string} original - The original string to encode.
 * @returns {string} The Base64 URL encoded string.
 */
function encodeBase64Url(original) {
  return encodeBase64UrlFromBuffer(Buffer.from(original));
}

/**
 * Encodes a buffer into Base64 URL format.
 * @param {Buffer} buffer - The buffer to encode.
 * @returns {string} The Base64 URL encoded string.
 */
function encodeBase64UrlFromBuffer(buffer) {
  return buffer
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=/g, "");
}

```

#### Python Code Snippet:

```python
import base64
import json
import os
from Crypto.PublicKey import RSA
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.Hash import SHA256
from Crypto.Signature import pkcs1_15

from keyprovider import read_juspay_public_key, read_merchant_private_key, get_key_uuid


def encode_base64url(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()


def decode_base64url(data: str) -> bytes:
    padding = '=' * (-len(data) % 4)
    return base64.urlsafe_b64decode(data + padding)


def sign(claims_json: str, key_id: str, private_key: RSA.RsaKey) -> str:
    header = {
        "alg": "RS256",
        "kid": key_id
    }
    header_b64 = encode_base64url(json.dumps(header).encode())
    payload_b64 = encode_base64url(claims_json.encode())
    message = f"{header_b64}.{payload_b64}"

    hash_value = SHA256.new(message.encode())
    signature = pkcs1_15.new(private_key).sign(hash_value)
    signature_b64 = encode_base64url(signature)

    return f"{header_b64}.{payload_b64}.{signature_b64}"


def encrypt(jwt: str, key_id: str, public_key: RSA.RsaKey) -> str:
    headers = {
        "alg": "RSA-OAEP",
        "enc": "A256GCM",
        "cty": "JWT",
        "kid": key_id
    }
    aad = encode_base64url(json.dumps(headers).encode())

    cek = os.urandom(32)
    iv = os.urandom(12)

    cipher_rsa = PKCS1_OAEP.new(public_key)
    encrypted_key = cipher_rsa.encrypt(cek)

    cipher_aes = AES.new(cek, AES.MODE_GCM, nonce=iv)
    cipher_aes.update(aad.encode())
    ciphertext, tag = cipher_aes.encrypt_and_digest(jwt.encode())

    return f"{aad}.{encode_base64url(encrypted_key)}.{encode_base64url(iv)}.{encode_base64url(ciphertext)}.{encode_base64url(tag)}"


def jwt_encrypt(claims: dict, key_id: str, public_key: RSA.RsaKey, private_key: RSA.RsaKey) -> str:
    signed_jwt = sign(json.dumps(claims), key_id, private_key)
    return encrypt(signed_jwt, key_id, public_key)


if __name__ == "__main__":
    claims = {
        "order_id": "test_2340",
        "amount": 1
    }

    pub_key = read_juspay_public_key()
    priv_key = read_merchant_private_key()
    key_id = get_key_uuid()

    encrypted_token = jwt_encrypt(claims, key_id, pub_key, priv_key)
    print(json.dumps({"JWT": encrypted_token}))

```

#### Java Code Snippet:

```java

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSAEncrypter;
import com.nimbusds.jose.crypto.RSASSASigner;

import java.security.PrivateKey;
import java.security.interfaces.RSAPublicKey;

public class Encrypt {
    /**
     * Encrypts a request payload using JWS (signing) and JWE (encryption) standards.
     * <p>
     * Steps:
     * 1. Signs the payload using the merchant's RSA private key (JWS - RS256).
     * 2. Encrypts the signed payload using Juspay's RSA public key (JWE - RSA-OAEP + AES256-GCM).
     *
     * @param requestPayloadStr The JSON string payload to be securely signed and encrypted.
     * @return A compact serialized JWE string containing the encrypted and signed payload.
     * @throws Exception if signing or encryption fails, or key reading fails.
     */
    public static String jwtEncrypt(String requestPayloadStr) throws Exception {
        PrivateKey merchantPrivateKey = KeyProvider.readMerchantPrivateKey();
        RSAPublicKey juspayPublicKey = KeyProvider.readJuspayPublicKey();
        Payload jwspayload = new Payload(requestPayloadStr);
        JWSHeader jwsheader = new JWSHeader.Builder(
                JWSAlgorithm.RS256
        ).keyID(
                KeyProvider.getKeyUUID()
        ).build();
        JWSObject jwsObject = new JWSObject(jwsheader, jwspayload);
        JWSSigner signer = new RSASSASigner(merchantPrivateKey);
        jwsObject.sign(signer);
        Payload jwepayload = new Payload(jwsObject.serialize());
        JWEHeader jweheader = new JWEHeader.Builder(
                JWEAlgorithm.RSA_OAEP_256,
                EncryptionMethod.A256GCM
        ).keyID(
                KeyProvider.getKeyUUID()
        ).build();
        JWEObject jweObject = new JWEObject(jweheader, jwepayload);
        JWEEncrypter encrypt = new RSAEncrypter(juspayPublicKey);
        jweObject.encrypt(encrypt);
        return jweObject.serialize();
    }

    public static void main(String[] args) throws Exception {
        String claims = "{ \"order_id\" : \"test_2340\", \"amount\": 1}";
        String jwtToken = jwtEncrypt(claims);
        String httpRequestPayload = "{\"JWT\": \"" + jwtToken + "\"}";
        System.out.println(httpRequestPayload);
    }
}
```

#### PHP Code Snippet:

```php
<?php

require_once 'vendor/autoload.php';
require_once 'keyprovider.php';

use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Encryption\Algorithm\ContentEncryption\A256GCM;
use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP256;
use Jose\Component\Encryption\JWEBuilder;
use Jose\Component\Encryption\Serializer\CompactSerializer as JweCompactSerializer;
use Jose\Component\Signature\Algorithm\RS256;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer;

/**
 * Encrypts a request payload using JWS (signing) and JWE (encryption) standards.
 * 1. Signs the payload using the merchant's private key (JWS - RS256).
 * 2. Encrypts the signed payload using the Juspay public key (JWE - RSA-OAEP + A256GCM).
 *
 * @param string $requestPayloadStr JSON string to encrypt
 * @return string Encrypted JWE string
 * @throws Exception if encryption fails
 */
function jwtEncrypt(string $requestPayloadStr): string
{
    $merchantPrivateKey = KeyProvider::readMerchantPrivateKey();
    $juspayPublicKey = KeyProvider::readJuspayPublicKey();

    $jwsAlgorithmManager = new AlgorithmManager([new RS256()]);
    $jwsBuilder = new JWSBuilder($jwsAlgorithmManager);
    $jws = $jwsBuilder
        ->create()
        ->withPayload($requestPayloadStr)
        ->addSignature($merchantPrivateKey, [
            'alg' => 'RS256',
            'kid' => KeyProvider::getKeyUUID()
        ])
        ->build();

    $jwsSerializer = new JwsCompactSerializer();
    $jwsString = $jwsSerializer->serialize($jws, 0);

    $jweKeyEncryptionAlgorithmManager = new AlgorithmManager([new RSAOAEP256()]);
    $jweContentEncryptionAlgorithmManager = new AlgorithmManager([new A256GCM()]);

    $jweBuilder = new JWEBuilder(
        $jweKeyEncryptionAlgorithmManager,
        $jweContentEncryptionAlgorithmManager,
        null
    );

    $jwe = $jweBuilder
        ->create()
        ->withPayload($jwsString)
        ->withSharedProtectedHeader([
            'alg' => 'RSA-OAEP-256',
            'enc' => 'A256GCM',
            'kid' => KeyProvider::getKeyUUID()
        ])
        ->addRecipient($juspayPublicKey)
        ->build();

    $jweSerializer = new JweCompactSerializer();
    return $jweSerializer->serialize($jwe, 0);
}

if (php_sapi_name() === 'cli' || isset($_SERVER['argv'])) {
    $claims = json_encode([
        "order_id" => "test_2340",
        "amount" => 1,
    ]);

    try {
        $jwtToken = jwtEncrypt($claims);
        $httpRequestPayload = json_encode(["JWT" => $jwtToken], JSON_UNESCAPED_SLASHES);
        echo $httpRequestPayload . PHP_EOL;
    } catch (Exception $e) {
        echo "Encryption failed: " . $e->getMessage() . PHP_EOL;
    }
}

```

#### C# Code Snippet:

```c#
using Jose;
using System.Collections.Generic;
using System.Security.Cryptography;

public static class Encrypt
{
    public static string jwtEncrypt(string payload)
    {
        RSA merchantPrivateKey = KeyProvider.readMerchantPrivateKey();
        RSA juspayPublicKey = KeyProvider.readJuspayPublicKey();

        var jwsHeaders = new Dictionary<string, object>
        {
            { "kid", KeyProvider.getKeyUUID() }
        };

        string signedPayload = JWT.Encode(payload, merchantPrivateKey, JwsAlgorithm.RS256, extraHeaders: jwsHeaders);

        var jweHeaders = new Dictionary<string, object>
        {
            { "kid", KeyProvider.getKeyUUID() }
        };

        string encryptedPayload = JWT.Encode(signedPayload, juspayPublicKey, JweAlgorithm.RSA_OAEP_256, JweEncryption.A128GCM, extraHeaders: jweHeaders);

        return encryptedPayload;
    }
}

```



### JWT Decryption



Pseudo Code


#### Shell Code Snippet:

```shell
Juspay:
   [Decide Keys]
      ↓ For Http request it'll use same key_uuid and for webhooks it'll use the one that is configured
   [Payload]
      ↓ stringify the request and make JWS Headers {alg=RS256, kid=key_uuid}
      ↓ sign with Juspay's private key
   [JWS Token]
      ↓ make JWE Headers {enc=RSA-256-OAEP, alg=A256GCM, kid=key_uuid, cty=JWT}
      ↓ encrypt raw JWS Token with Merchant's public key
   [JWE Token]
      ↓ make request payload {"JWT": "JWE Token"}
   [Response Body] → Sent to Merchant

Merchant:
   ↓ decrypt with Merchant's private key
   ↓ verify signature using Juspay's public key
   → Trusted, confidential data

```


Demo Decryption Code


#### NodeJS Code Snippet:

```nodejs
const crypto = require("crypto");
const { readMerchantPrivateKey, readJuspayPublicKey } = require("./keyprovider");

(async () => {
  const apiResponse = {
    JWT : "xxxxxxxxxxxxxxxxxxxxxx" // Add Message here
  };
  const JWTToken   = apiResponse.JWT;
  const publicKey  = readJuspayPublicKey();
  const privateKey = readMerchantPrivateKey();

  const decryptedResponse = jwtDecrypt(JWTToken, publicKey, privateKey);
  console.log(decryptedResponse);
})();

// UTILS
/**
 * Decodes a Base64 URL encoded string.a
 * @param {string} base64url - The Base64 URL encoded string to decode.
 * @returns {string} The decoded string.
 */
function decodeBase64Url(base64url) {
  return decodeBase64UrlToBuffer(base64url).toString();
}

/**
 * Decodes a Base64 URL encoded string into a buffer.
 * @param {string} base64url - The Base64 URL encoded string to decode.
 * @returns {Buffer} The decoded buffer.
 */
function decodeBase64UrlToBuffer(base64url) {
  // Decode the Base64 URL encoded string to a Base64 encoded string
  const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
  return Buffer.from(base64, 'base64');
}

// DECRYPT
/**
 * Decrypts the given encrypted token.
 * @param {string} cipher - Encrypted token or string representation of it.
 * @param {crypto.KeyObject} privateKey - Private key.
 * @returns {string} Decrypted data.
 * @throws {Error} If decryption fails.
 */
function decrypt(cipher, privateKey) {
  const cipherParts = cipher.split(".");
  if (cipherParts.length !== 5) {
    throw new Error("EncryptedCipherIllformed");
  }
  const data = {
    header: cipherParts[0],
    encryptedKey: cipherParts[1],
    iv: cipherParts[2],
    encryptedPayload: cipherParts[3],
    tag: cipherParts[4],
  };
  const aad = Buffer.from(data.header);
  const encryptedKey = decodeBase64UrlToBuffer(data.encryptedKey);
  const iv = decodeBase64UrlToBuffer(data.iv);
  const encryptedPayload = decodeBase64UrlToBuffer(data.encryptedPayload);
  const tag = decodeBase64UrlToBuffer(data.tag);
  const cekOptions = {
    key: privateKey,
    oaepHash: "sha256",
    padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
  };
  const cek = crypto.privateDecrypt(cekOptions, encryptedKey);
  const decipher = crypto.createDecipheriv("aes-256-gcm", cek, iv);
  decipher.setAutoPadding(false);
  decipher.setAAD(aad);
  decipher.setAuthTag(tag);
  const cipherOutput = Buffer.concat([
    decipher.update(encryptedPayload),
    decipher.final(),
  ]);
  return decodeBase64Url(cipherOutput.toString("base64"));
}

/**
 * Verifies the given signed object or string.
 * @param {string} signed - Signed object or string representation of it.
 * @param {crypto.KeyObject} publicKey - Public key.
 * @returns {string} Verified payload.
 * @throws {Error} If verification fails.
 */
function verify(signed, publicKey) {
  const signedParts = signed.split(".");
  if (signedParts.length !== 3) {
    throw new Error("SignatureIllformed");
  }
  const data = {
    header: signedParts[0],
    payload: signedParts[1],
    signature: signedParts[2],
  };
  const verifier = crypto.createVerify("RSA-SHA256");
  const protect = `${data.header}.${data.payload}`;
  verifier.update(protect);
  const isVerified = verifier.verify(publicKey, data.signature, "base64");
  if (isVerified) {
    return decodeBase64Url(data.payload);
  } else {
    throw new Error("SignatureValidationFailed");
  }
}

/**
 * Decrypts the given data and returns the decrypted string.
 * @param {string} data - Data to decrypt.
 * @param {crypto.KeyObject} publicKey - Public key.
 * @param {crypto.KeyObject} privateKey - Private key.
 * @returns {string} Decrypted data.
 */
function jwtDecrypt(data, publicKey, privateKey) {
  const signed = decrypt(data, privateKey);
  return verify(signed, publicKey);
}

```

#### Python Code Snippet:

```python
import base64
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256

from keyprovider import read_juspay_public_key, read_merchant_private_key, get_key_uuid


def base64url_decode(data: str) -> bytes:
    padding = '=' * (-len(data) % 4)
    return base64.urlsafe_b64decode(data + padding)


def decrypt(jwe_token: str, private_key: RSA.RsaKey) -> str:
    parts = jwe_token.split('.')
    if len(parts) != 5:
        raise ValueError("EncryptedCipherIllformed")

    header_b64, encrypted_key_b64, iv_b64, cipher_text_b64, tag_b64 = parts

    aad = header_b64.encode()
    encrypted_key = base64url_decode(encrypted_key_b64)
    iv = base64url_decode(iv_b64)
    ciphertext = base64url_decode(cipher_text_b64)
    tag = base64url_decode(tag_b64)

    rsa_cipher = PKCS1_OAEP.new(private_key, hashAlgo=SHA256)
    cek = rsa_cipher.decrypt(encrypted_key)

    aes_cipher = AES.new(cek, AES.MODE_GCM, nonce=iv)
    aes_cipher.update(aad)
    plaintext = aes_cipher.decrypt_and_verify(ciphertext, tag)

    return plaintext.decode()


def verify(jws_token: str, public_key: RSA.RsaKey) -> str:
    parts = jws_token.split('.')
    if len(parts) != 3:
        raise ValueError("SignatureIllformed")

    header_b64, payload_b64, signature_b64 = parts
    signing_input = f"{header_b64}.{payload_b64}".encode()
    signature = base64url_decode(signature_b64)

    hash_obj = SHA256.new(signing_input)

    try:
        pkcs1_15.new(public_key).verify(hash_obj, signature)
        return base64url_decode(payload_b64).decode()
    except (ValueError, TypeError):
        raise Exception("SignatureValidationFailed")


def jwt_decrypt(jwe_token: str, public_key_pem: RSA.RsaKey, private_key_pem: RSA.RsaKey) -> str:
    jws_signed = decrypt(jwe_token, private_key_pem)
    return verify(jws_signed, public_key_pem)


if __name__ == "__main__":
    api_response = {
        "JWT": "xxxxxxxxxxxxxxxxxxxxxx"
    }

    pub_key = read_juspay_public_key()
    priv_key = read_merchant_private_key()
    key_id = get_key_uuid()

    decrypted_response = jwt_decrypt(api_response["JWT"], pub_key, priv_key)
    print(decrypted_response)

```

#### Java Code Snippet:

```java
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSADecrypter;
import com.nimbusds.jose.crypto.RSASSAVerifier;

import java.security.PrivateKey;
import java.security.interfaces.RSAPublicKey;

public class Decrypt {
    /**
     * Decrypts and verifies a received JWT string.
     * <p>
     * Steps:
     * 1. Decrypts the JWE using the merchant's RSA private key.
     * 2. Verifies the JWS signature using Juspay's RSA public key.
     *
     * @param responsePayloadStr The compact serialized JWE string received from Juspay.
     * @return The original signed payload string if signature verification succeeds.
     * @throws Exception if decryption or signature verification fails, or keys cannot be loaded.
     * @throws RuntimeException if the JWS signature is invalid.
     */
    public static String jwtDecrypt(String responsePayloadStr) throws Exception {
        PrivateKey merchantPrivateKey = KeyProvider.readMerchantPrivateKey();
        RSAPublicKey juspayPublicKey = KeyProvider.readJuspayPublicKey();
        JWEObject jweObject = JWEObject.parse(responsePayloadStr);
        JWEDecrypter decrypter = new RSADecrypter(merchantPrivateKey);
        jweObject.decrypt(decrypter);
        String jwepayload = jweObject.getPayload().toString();
        JWSObject jwsObject = JWSObject.parse(jwepayload);
        boolean isSignatureValid = jwsObject.verify(new RSASSAVerifier(juspayPublicKey));
        if (!isSignatureValid) {
            throw new RuntimeException("Cannot verify signature");
        }
        return jwsObject.getPayload().toString();
    }

    public static void main(String[] args) throws Exception {
        String jwtToken = "xxxxxxxxxxxxxxxxxxxxxx"; // place JWT value in this
        System.out.println(jwtDecrypt(jwtToken));
    }
}

```

#### PHP Code Snippet:

```php
<?php

require_once 'vendor/autoload.php';
require_once 'keyprovider.php';

use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Encryption\Algorithm\ContentEncryption\A256GCM;
use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP256;
use Jose\Component\Encryption\JWEDecrypter;
use Jose\Component\Encryption\Serializer\CompactSerializer as JweCompactSerializer;
use Jose\Component\Encryption\Serializer\JWESerializerManager;
use Jose\Component\Signature\Algorithm\RS256;
use Jose\Component\Signature\JWSVerifier;
use Jose\Component\Signature\Serializer\CompactSerializer as JwsCompactSerializer;

function jwtDecrypt(string $payload): string {
    $merchantPrivateKey = KeyProvider::readMerchantPrivateKey();
    $juspayPublicKey = KeyProvider::readJuspayPublicKey();

    $jweKeyEncryptionAlgorithmManager = new AlgorithmManager([new RSAOAEP256()]);
    $jweContentEncryptionAlgorithmManager = new AlgorithmManager([new A256GCM()]);

    $jweDecrypter = new JWEDecrypter(
        $jweKeyEncryptionAlgorithmManager,
        $jweContentEncryptionAlgorithmManager,
        null
    );
    $jweSerializerManager = new JWESerializerManager([
        new JweCompactSerializer(),
    ]);
    $jweObject = $jweSerializerManager->unserialize($payload);
    $jweDecrypter->decryptUsingKey($jweObject, $merchantPrivateKey, 0);
    $jweDecyptedPayload = $jweObject->getPayload();

    $jwsAlgorithmManager = new AlgorithmManager([new RS256()]);
    $jwsVerifier = new JWSVerifier($jwsAlgorithmManager);
    $jwsSerializer = new JwsCompactSerializer();
    $jwsObject = $jwsSerializer->unserialize($jweDecyptedPayload);
    $isVerified = $jwsVerifier->verifyWithKey(
        $jwsObject,
        $juspayPublicKey,
        0
    );
    $jwsPayload = $jwsObject->getPayload();

    return $jwsPayload;
}

if (php_sapi_name() === 'cli' || isset($_SERVER['argv'])) {
    $jwtToken = "Your JWE Token here, it's value of JWT key"; // header.encryptedPayload.iv.encryptedKey.tag

    try {
        $decryptedResponse = jwtDecrypt($jwtToken);
        echo $decryptedResponse . PHP_EOL;
    } catch (Exception $e) {
        echo "Encryption failed: " . $e->getMessage() . PHP_EOL;
    }
}

```

#### C# Code Snippet:

```c#
using Jose;

using System.Collections.Generic;
using System.Security.Cryptography;

public static class Decrypt
{
    public static string jwtDecrypt(string responsePayloadStr)
    {
        RSA merchantPrivateKey = KeyProvider.readMerchantPrivateKey();
        RSA juspayPublicKey = KeyProvider.readJuspayPublicKey();

        string decrypted = JWT.Decode(responsePayloadStr, merchantPrivateKey);
        string verified = JWT.Decode(decrypted, juspayPublicKey);

        return verified;
    }
}

```



## Download



You can download the above codes using the following [link](https://dth95m2xtyv8v.cloudfront.net/tesseract/assets/api-reference/jwe-encryptions%20(4).zip).


## Key Rotations



Key rotations for **JWT**  Encryption (Both API Request and Webhooks) are typically straightforward, thanks to the JOSE™ framework that contains protected kid information inside **JWE™**  and **JWS™**  headers. Merchants and Juspay can now mutually agree on key pairs using kid, enabling seamless key rotations with minimal changes at Merchant’s end (and possibly without any downtime).


## Summary



![Image](https://dth95m2xtyv8v.cloudfront.net/tesseract/assets/api-reference/-Gtbs8.png)



---

## See Also

- [Juspay API Error Codes](https://juspay.io/in/docs/api-reference/docs/express-checkout/juspay-api-error-codes)
- [PG Unified Error Codes](https://juspay.io/in/docs/api-reference/docs/express-checkout/pg-unified-error-codes)
