Skip to content

Preparing the Request JWT

To send a /par request, you must first construct a signed Request JWT (also called a Request Object or JAR — JWT Authorization Request). This JWT is a signed package of all authorization parameters, proving they came from your registered application and haven't been tampered with.

Request JWT highlighted in the PAR flow

Strict claim rules

For a precise per-claim reference covering aud, exp/nbf lifetime windows, clock skew, and the difference between the Request Object and Client Assertion, see JWT Claim Rules.

json
{
  "alg": "PS256",
  "kid": "<your-signing-key-id>"
}
FieldValueDescription
algPS256Signing algorithm — RSA-PSS with SHA-256
kidstringKey ID of your signing certificate, as registered in the Trust Framework

Payload Claims

ClaimTypeRequiredDescriptionExample
audstringThe issuer of the Authorization Server — found via API Discoveryhttps://auth1.[LFICode].apihub.openfinance.ae
iatnumberIssued At Unix timestamp — when the JWT was created1713196113
expnumberExpiry as a Unix timestamp. Must be shortly after nbf — maximum 5 minutes1713196423
issstringYour application's Client ID from the Trust Frameworkyour-client-id
client_idstringYour application's Client ID (same as iss)your-client-id
redirect_uristringThe callback URI registered in your Trust Framework applicationhttps://yourapp.com/callback
scopestringSpace-separated OAuth 2.0 scopesaccounts openid
noncestringRandom UUID — prevents replay attacks by binding the ID token to this requesta1b2c3d4-...
statestringRandom UUID — returned in the redirect; prevents CSRF attackse5f6g7h8-...
nbfnumberNot Before Unix timestamp. Set slightly before iat (e.g. iat - 10) to allow for clock skew1713196103
response_typestringMust be codecode
code_challenge_methodstringPKCE method — only S256 is supportedS256
code_challengestringBase64url-encoded SHA-256 hash of your code_verifierE9Melhoa2Ow...
max_agenumberMaximum age (seconds) of the user's existing authentication session. Capped at 36003600
authorization_detailsarrayDescribes what the user is consenting to — see Consent[{...}]

Generating a PKCE Code Challenge

Before building the JWT, generate a code_verifier and derive the code_challenge from it:

typescript
import crypto from 'node:crypto'

// Generate a cryptographically random code_verifier (43–128 chars, URL-safe)
export function generateCodeVerifier(): string {
  return crypto.randomBytes(32).toString('base64url')
}

// Derive the code_challenge (S256 = SHA-256 of the verifier, base64url-encoded)
export function deriveCodeChallenge(verifier: string): string {
  return crypto.createHash('sha256').update(verifier).digest('base64url')
}

Store the code_verifier securely — you'll need it when exchanging the authorization code for tokens.

Building the Request JWT

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

const ALGORITHM   = 'PS256'
const KEY_ID      = process.env.SIGNING_KEY_ID!
const CLIENT_ID   = process.env.CLIENT_ID!
const ISSUER      = process.env.AUTHORIZATION_SERVER_ISSUER!  // from .well-known
const REDIRECT_URI = process.env.REDIRECT_URI!

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

interface RequestJWTOptions {
  scope: string
  codeChallenge: string
  authorizationDetails: unknown[]
  maxAge?: number
}

export async function buildRequestJWT({
  scope,
  codeChallenge,
  authorizationDetails,
  maxAge = 3600,
}: RequestJWTOptions): Promise<string> {
  const now = Math.floor(Date.now() / 1000)

  return new SignJWT({
    // Authorization Server identity
    aud: ISSUER,

    // Client identity
    iss: CLIENT_ID,
    client_id: CLIENT_ID,

    // Authorization parameters
    scope,
    redirect_uri: REDIRECT_URI,
    response_type: 'code',

    // PKCE
    code_challenge_method: 'S256',
    code_challenge: codeChallenge,

    // Security
    nonce: crypto.randomUUID(),
    state: crypto.randomUUID(),
    max_age: maxAge,

    // Consent
    authorization_details: authorizationDetails,

    // Timing
    iat: now,
    nbf: now - 10,
    exp: now + 300,  // 5-minute expiry
  })
    .setProtectedHeader({ alg: ALGORITHM, kid: KEY_ID })
    .sign(privateKey)
}

Full Example

typescript
import { generateCodeVerifier, deriveCodeChallenge } from './pkce'
import { buildRequestJWT } from './request-jwt'

// 1. Generate PKCE pair
const codeVerifier  = generateCodeVerifier()
const codeChallenge = deriveCodeChallenge(codeVerifier)

// 2. Build the authorization_details (example: bank data sharing consent)
const authorizationDetails = [
  {
    type: 'urn:openfinanceuae:account-access-consent:v2.1',
    consent: {
      ConsentId: crypto.randomUUID(),
      ExpirationDateTime: new Date(Date.now() + 364 * 24 * 60 * 60 * 1000).toISOString(),
      Permissions: ['ReadAccountsBasic', 'ReadBalances', 'ReadTransactionsBasic'],
      OpenFinanceBilling: {
        UserType: 'Retail',
        Purpose: 'AccountAggregation',
      },
    },
  },
]

// 3. Build and sign the Request JWT
const requestJWT = await buildRequestJWT({
  scope: 'accounts openid',
  codeChallenge,
  authorizationDetails,
})

// 4. Send to /par
// Endpoints are read from .well-known/openid-configuration —
// not constructed from the issuer URL (they live on different hosts).
const response = await fetch(discoveryDoc.pushed_authorization_request_endpoint, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: new URLSearchParams({ request: requestJWT }),
})
const { request_uri, expires_in } = await response.json()

// 5. Redirect the user
const authorizeUrl = new URL(discoveryDoc.authorization_endpoint)
authorizeUrl.searchParams.set('client_id', CLIENT_ID)
authorizeUrl.searchParams.set('request_uri', request_uri)
window.location.href = authorizeUrl.toString()

TIP

Store codeVerifier in your session — you'll need it at the /token endpoint to exchange the authorization code for access tokens.

Encrypting the Request JWT

If the LFI requires encrypted request objects, wrap the signed JWT in a JWE before sending to /par. See Message Encryption.