Security · FAPI · JWE

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.

01 Structure of an Encrypted Payload

Five base64url parts joined by dots

A compact-serialised JWE consists of five base64url-encoded parts joined by .:

Compact serialization
base64url(header) . base64url(encrypted-key) . base64url(iv) . base64url(ciphertext) . base64url(tag)
03 Discover the LFI's encryption key

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:

JWKS URI
https://keystore.directory.openfinance.ae/[LFI-UUID]/application.jwks

Step 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:

application.jwks (excerpt)json
{
  "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"
}
Key selection

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.

04 Encrypt the PII Payload

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.

typescript
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)
}
Testing encryption on the sandbox

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.

05 Embed the JWE in the Consent

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.

typescript
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.

06 Receiving a JWE

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.