Security · FAPI · JWS

Message Signing (JWS) 2 min read

A JWS (JSON Web Signature — RFC 7515) is the cryptographic mechanism that signs a JSON payload to prove two things:

  • Authenticity — it genuinely came from the holder of the private key
  • Integrity — the content has not been modified since it was signed

In UAE Open Finance, signing is required whenever your application sends a JWT to an Authorization Server:

  • The Request Object sent to /par
  • The Client Assertion sent to /token
01 Structure of a Signed JWT

Three base64url parts joined by dots

A signed JWT consists of three base64url-encoded parts joined by .:

Compact serialization
base64url(header) . base64url(payload) . base64url(signature)
03 Payload

Domain claims plus FAPI-required timing claims

The payload is a JSON object of claims. The structure depends on the use case — see Request JWT and Client Assertion for the specific claim sets.

All signed JWTs must include timing claims to prevent replay attacks:

ClaimDescription
iatIssued At — current Unix timestamp
nbfNot Before — slightly before iat to allow for clock skew (e.g. iat - 10)
expExpiry — short-lived; typically 5 minutes (iat + 300)
04 Prerequisites

What you need before signing

  • An application registered in the Trust Framework with an appropriate role
  • A valid signing certificate and its corresponding private key
  • The Key ID (kid) of your signing certificate from the Trust Framework
05 Signing a JWT

Reusable signer for FAPI-compliant JWS

The Node.js example uses the jose library (available for Node.js, browsers, Deno, and Cloudflare Workers); the Python example uses PyJWT.

typescript
import { SignJWT, importPKCS8 } from 'jose'
import { readFileSync } from 'node:fs'

const ALGORITHM = 'PS256'
const KEY_ID = process.env.SIGNING_KEY_ID!      // kid from Trust Framework
const CLIENT_ID = process.env.CLIENT_ID!          // your application's client_id
const ISSUER = process.env.AUTHORIZATION_SERVER_ISSUER! // from .well-known

const privateKeyPem = readFileSync('./certificates/signing.key', 'utf8')
const privateKey = await importPKCS8(privateKeyPem, ALGORITHM)

/**
 * Sign a payload as a FAPI-compliant JWS.
 * Caller provides the domain-specific claims; timing claims are added automatically.
 */
export async function signJWT(
  claims: Record<string, unknown>,
  expiresInSeconds = 300
): Promise<string> {
  const now = Math.floor(Date.now() / 1000)

  return new SignJWT({
    ...claims,
    iat: now,
    nbf: now - 10,       // 10-second clock skew buffer
    exp: now + expiresInSeconds,
  })
    .setProtectedHeader({ alg: ALGORITHM, kid: KEY_ID })
    .sign(privateKey)
}
Removing whitespace from PEM keys

Some environments require the PEM key to have no line breaks when passed as an environment variable. Strip them with:

bashbash
awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' signing.key
Testing signing on the sandbox

The sandbox provides O3 Utility endpoints that accept your private key and return ready-made signed JWTs — useful for validating your signing setup before writing your own code. See O3 Sandbox Utilities.

06 Verifying a Signature

Useful for testing your own setup

LFIs will verify your signatures using your public key fetched from your application's JWKS URI in the Trust Framework. You do not need to implement verification yourself, but it is useful for testing:

typescript
import { jwtVerify, createRemoteJWKSet } from 'jose'

const JWKS = createRemoteJWKSet(
  new URL('https://keystore.directory.openfinance.ae/[your-org-id]/application.jwks')
)

const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
  algorithms: ['PS256'],
})