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:
Three base64url parts joined by dots
A signed JWT consists of three base64url-encoded parts joined by .:
base64url(header) . base64url(payload) . base64url(signature)Algorithm and signing key identifier
{
"alg": "PS256",
"kid": "<your-signing-key-id>"
}| Field | Value | Description |
|---|---|---|
alg | PS256 | RSA-PSS with SHA-256. The only algorithm supported by the UAE Open Finance FAPI profile |
kid | string | The Key ID of your signing certificate, as registered in the Trust Framework |
Your kid is assigned by the Trust Framework when your signing certificate is issued. Find it on the certificate detail page: Application → App Certificates → select the certificate. See Finding Your Key ID for a screenshot.
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:
| Claim | Description |
|---|---|
iat | Issued At — current Unix timestamp |
nbf | Not Before — slightly before iat to allow for clock skew (e.g. iat - 10) |
exp | Expiry — short-lived; typically 5 minutes (iat + 300) |
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
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.
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)
}Some environments require the PEM key to have no line breaks when passed as an environment variable. Strip them with:
awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' signing.keyThe 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.
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:
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'],
})