Message Encryption (JWE) 3 min read
A JWE (JSON Web Encryption — RFC 7516) is the cryptographic mechanism that encrypts a payload so that only the intended recipient can read it.
In UAE Open Finance, JWE is used specifically for payment consents — to encrypt the consent's PII payload (creditor and debtor names, account numbers, risk indicators) end-to-end with the LFI's public key. The encrypted blob is carried in the consent's pii field, nested inside the Request JWT's authorization_details. The Request JWT itself is signed (JWS) but is not wrapped in a JWE; only the pii field is.
See Payment PII Encryption for the data privacy rationale and the LFI/TPP validation responsibilities that flow from this design.
Five base64url parts joined by dots
A compact-serialised JWE consists of five base64url-encoded parts joined by .:
base64url(header) . base64url(encrypted-key) . base64url(iv) . base64url(ciphertext) . base64url(tag)Key wrap, content encryption, and the LFI's key ID
{
"alg": "RSA-OAEP-256",
"enc": "A256GCM",
"kid": "<lfi-encryption-key-id>"
}| Field | Value | Description |
|---|---|---|
alg | RSA-OAEP-256 | Key-wrapping algorithm — encrypts the content encryption key using the LFI's RSA public key |
enc | A256GCM | Content encryption algorithm — encrypts the actual payload using AES-256-GCM |
kid | string | Key ID of the LFI's encryption key, taken from their JWKS |
JWKS URI and key selection
Step 1 — Discover the JWKS URI
Each LFI publishes its public keys at a JWKS URI. You can find this URI by performing API Discovery via the .well-known endpoint.
The JWKS URI follows this format:
https://keystore.directory.openfinance.ae/[LFI-UUID]/application.jwksStep 2 — Select the encryption key
Fetch the JWKS and find a key where "use": "enc". This is the LFI's public key intended for encryption.
Example encryption key from a JWKS:
{
"kty": "RSA",
"use": "enc",
"x5c": ["MIIF5zCCBM+gAwIBAgIUTAsBRMW3lPiptQPq4DD3aPVvT/gwDQYJKoZIhvcNAQEL..."],
"n": "qyWmUY-_eWY7H8IHeHDTM-UIgJUVeoEme1J2KCvMMzmUDLSRUP8HljchOQKx9zwMquOSEXsQC4IOsXOa2NKPFbeidhnzSire6nHALJMowN3fMfIeBTbf9nuEzafJHMLixSpcjrPvyhzhMKjGZ5EY6MCBm6fNdRmcEOBCTfF8wjOrl9Y4mi-fz16INi8zHJrsKMJwuj3VD5jQ3J64twLQ-E9aECuIBH51L-6J4c9Pwp1M3W_nZ0RpivBQlLY8jJKr_r-a9TUKikFzVSWK9-trvMK32fLjuEwTvSB9YHLPfOq8qNmyS8djf8vM2AIavkE5Ge-ZRGr0JXXbS5vEAOUHkw",
"e": "AQAB",
"kid": "NLVWCFEnbvtP1n4mG040VTwTMl-mhI6AdQiOOJbf_1w",
"x5u": "https://keystore.directory.openfinance.ae/36b067c3-8017-4144-bb7e-49cf794089c9/NLVWCFEnbvtP1n4mG040VTwTMl-mhI6AdQiOOJbf_1w.pem",
"x5t#S256": "NLVWCFEnbvtP1n4mG040VTwTMl-mhI6AdQiOOJbf_1w",
"x5dn": "OU=36b067c3-8017-4144-bb7e-49cf794089c9,O=Abu Dhabi Commercial Bank PBJC,C=AE"
}If the JWKS contains multiple keys, always select the one where "use": "enc". Do not use a signing key ("use": "sig") for encryption — the operations are not interchangeable.
Produce a compact JWE from the PII JSON
The plaintext to encrypt is the PII JSON object — the Initiation and RiskIndicators structures defined by the consent schema. Encrypt it as a compact JWE before placing the result in the consent's pii field.
import { importJWK, CompactEncrypt } from 'jose'
interface JWK {
use: string
kid: string
[key: string]: unknown
}
/**
* Encrypt a payment PII payload as a compact JWE using the LFI's public key.
* @param pii - The PII JSON (Initiation, RiskIndicators, etc.)
* @param jwksUri - The LFI's JWKS URI from .well-known
*/
export async function encryptPii(
pii: Record<string, unknown>,
jwksUri: string,
): Promise<string> {
// 1. Fetch the LFI's public keys
const response = await fetch(jwksUri)
const { keys } = await response.json() as { keys: JWK[] }
// 2. Find the encryption key
const encKeyJwk = keys.find((k) => k.use === 'enc')
if (!encKeyJwk) throw new Error('No encryption key (use: enc) found in JWKS')
// 3. Import the public key
const encKey = await importJWK(encKeyJwk, 'RSA-OAEP-256')
// 4. Encrypt the PII JSON
const plaintext = new TextEncoder().encode(JSON.stringify(pii))
return new CompactEncrypt(plaintext)
.setProtectedHeader({
alg: 'RSA-OAEP-256',
enc: 'A256GCM',
kid: encKeyJwk.kid,
})
.encrypt(encKey)
}The sandbox provides an O3 Utility endpoint that accepts your PII payload and the LFI's JWKS URL and returns a ready-made encrypted PII token — useful for validating your payload structure before writing your own encryption code. See O3 Sandbox Utilities.
The JWE goes in the consent's pii field, then the Request JWT is signed around it
Once you have the JWE string, place it in the pii field of the consent inside the Request JWT's authorization_details. The surrounding Request JWT is signed (JWS) as usual — only the pii field is encrypted.
import crypto from 'node:crypto'
import { encryptPii } from './encrypt-pii'
import { buildRequestJWT } from './request-jwt'
const jwksUri = 'https://keystore.directory.openfinance.ae/[lfi-uuid]/application.jwks'
// 1. Build the PII payload (creditor/debtor accounts, risk indicators)
const pii = {
Initiation: {
Debtor: { Name: { en: 'John Doe' } },
Creditor: [
{ Name: { en: 'Acme Ltd' }, Identification: 'AE070331234567890123456' },
],
},
RiskIndicators: { /* ... */ },
}
// 2. Encrypt the PII as a JWE
const piiJwe = await encryptPii(pii, jwksUri)
// 3. Embed the JWE inside the consent's authorization_details
const authorizationDetails = [{
type: 'urn:openfinanceuae:service-initiation-consent:v2.1',
consent: {
ConsentId: crypto.randomUUID(),
ExpirationDateTime: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
pii: piiJwe, // ← encrypted PII goes here
// other consent fields
},
}]
// 4. Build and sign the Request JWT — note: the Request JWT itself is NOT encrypted
const requestJwt = await buildRequestJWT({
scope: 'payments openid',
codeChallenge: '<from PKCE>',
authorizationDetails,
})
// 5. Send to /par as usual
// body: new URLSearchParams({ request: requestJwt }) Each encryption is fresh: a new payload is produced and encrypted at consent creation, and a fresh payload is produced and encrypted again at each POST/payments. The two payloads are independently validated by the LFI after decryption.
Inbound encrypted webhooks from the API Hub
For guidance on receiving and decrypting inbound JWEs from the API Hub — including key selection by kid, signature verification, and FAPI-required security checks — see Receiving Event Notifications.
