This is v2.2-rc1 — a release candidate for the v2.2 standards, published for review and not yet ratified. It MUST NOT be used as the basis for a production implementation. For the current standards, switch to v2.1 using the version selector. See the v2.1 → v2.2-rc1 changelog for every change in this version.
Verifying the TPP JWS Signature 3 min read
The PersonalIdentifiableInformation field is embedded within a request that the API Hub has already verified was signed by the TPP. The outer JWS signature confirms that the PII was submitted by the authenticated TPP and has not been modified in transit.
Verifying the inner JWS signature on the PII payload is therefore not required, but LFIs may choose to implement it as a defence-in-depth measure.
Reasons to verify the inner JWS
You may want to verify the TPP's JWS signature on the PII if:
- Your security policy requires independent verification of all signed payloads, regardless of upstream validation.
- You want to confirm the specific TPP identity that constructed the PII (available in the
issandsubclaims). - You are building an audit trail that requires cryptographic proof tied to the TPP's signing key.
Resolve the JWKS, verify the signature
The inner JWS (the result of decrypting the JWE) is signed by the TPP using their signing key. The JWS header contains the kid of the TPP's signing key, and the iss / sub claims identify the TPP's client_id.
Step 1 — Discover the TPP's JWKS
The TPP's public signing keys are published through the Trust Framework directory. You can resolve the TPP's JWKS URI using the o3-caller-software-statement-id header from the inbound request:
https://keystore.directory.openfinance.ae/[software-statement-id]/application.jwks
The URL above returns the TPP's currently active keys. That is sufficient only if you verify at /consent/action/validate or the post consent event, where the signature is seconds old — Pattern A on the decryption guide.
A TPP may rotate its signing key at any time, and a rotated kid leaves the active set. If you verify during the authorisation journey, on /consent/event/patch, or at POST /payments, you MUST fall back to the parallel inactive/ JWKS when the kid is not in the active set. See Verifying the PII Signature — Rotated Keys and the Inactive JWKS.
Published JWKS entries carry kty, use, kid and x5c, but no alg. Pin PS256 — the only signing algorithm in the UAE Open Finance FAPI profile — rather than accepting whatever the JWS header proposes.
Step 2 — Verify the JWS
import { createRemoteJWKSet, jwtVerify } from 'jose'
async function verifyTPPSignature(
jwsString: string,
softwareStatementId: string
): Promise<Record<string, unknown>> {
// 1. Build a JWKS resolver for the TPP's published keys
const jwksUri = `https://keystore.directory.openfinance.ae/${softwareStatementId}/application.jwks`
const jwks = createRemoteJWKSet(new URL(jwksUri))
// 2. Verify the signature and validate standard claims
const { payload } = await jwtVerify(jwsString, jwks)
return payload
}
Claims to validate
If you verify the JWS, you SHOULD also validate the following claims:
| Claim | Validation |
|---|---|
iss | Must match the TPP's client_id (available in the o3-caller-client-id request header) |
sub | Must match the TPP's client_id |
aud | Must contain your LFI's issuer identifier |
exp | Evaluate against the consent's CreationDateTime, not the current time — see below |
nbf | Evaluate against the consent's CreationDateTime, not the current time — see below |
iat | Must be in the past, and consistent with the consent's CreationDateTime |
jti | Record for replay detection if required by your security policy |
exp, iat and nbf on the PII JWS are set by the TPP when it signs the PII, and they bound the PAR submission window — not the moment you happen to be verifying. The same PII is replayed unchanged on every payment made under the consent, so on a long-lived consent those claims will normally have lapsed long before execution.
jwtVerify and its equivalents enforce exp and nbf against the current time by default. If you verify anywhere other than consent creation, evaluate them against the consent's CreationDateTime or disable the check — otherwise valid PII will start failing verification as consents age. Whether the consent is still usable today is answered by the API Hub's consent validation on every request, not by a claim inside the PII.
