Ozone

Open Finance Malaysia Developer Portal

Client Authentication Methods

Many PayNet API endpoints require client authentication to verify your identity. PayNet supports two client authentication methods: private_key_jwt and tls_client_auth (mTLS). This section covers both approaches.


Overview

Client authentication proves to the server that your application is authorized to make the request. The method you use depends on your technical preferences and environment.

Both methods are:

  • FAPI 2.0 compliant - Required by the PayNet API specification
  • Secure - Based on proven cryptographic standards
  • Supported across all endpoints - Use either method consistently

Method 1: private_key_jwt (Recommended)

With private_key_jwt, you create a signed JWT (assertion) that proves your identity using your signing private key. This method is self-contained and doesn't rely on TLS-level authentication.

How It Works

  1. Create a JWT with specific claims identifying your client
  2. Sign it with your signing private key (PS256 algorithm)
  3. Include the signed JWT in the client_assertion request parameter
  4. The server verifies the signature using your public signing certificate from your JWKS

JWT Structure

Header:

{
  "alg": "PS256",
  "kid": "{{KEY_ID}}"
}

Body:

{
  "iss": "{{CLIENT_ID}}",
  "sub": "{{CLIENT_ID}}",
  "aud": "{{TOKEN_ENDPOINT_URL}}",
  "iat": {{CURRENT_UNIX_TIMESTAMP}},
  "exp": {{CURRENT_UNIX_TIMESTAMP + 600}},
  "jti": "{{UUIDV4}}"
}

Field Definitions:

FieldDescription
algSigning algorithm. Must be PS256 (RSASSA-PSS with SHA-256)
kidKey ID of your signing certificate (from your JWKS)
issIssuer. Must be your client_id
subSubject. Must be your client_id
audAudience. The token endpoint URL (e.g., from well-known endpoint)
iatIssued At. Current Unix timestamp (in seconds)
expExpiration. Typically iat + 600 (10 minutes in future)
jtiJWT ID. Unique UUIDv4 to prevent replay attacks

Generating the Client Assertion

Node.js:

const jwt = require('jsonwebtoken');
const fs = require('fs');
const crypto = require('crypto');

const signingKey = fs.readFileSync('/path/to/signing-key.pem', 'utf8');

const now = Math.floor(Date.now() / 1000);
const assertion = {
  iss: '{{CLIENT_ID}}',
  sub: '{{CLIENT_ID}}',
  aud: '{{TOKEN_ENDPOINT_URL}}',
  iat: now,
  exp: now + 600,
  jti: crypto.randomUUID()
};

const options = {
  algorithm: 'PS256',
  keyid: '{{KEY_ID}}'
};

const clientAssertion = jwt.sign(assertion, signingKey, options);
console.log('client_assertion:', clientAssertion);

Python:

import jwt
import time
import uuid

signing_key = open('/path/to/signing-key.pem', 'r').read()

now = int(time.time())
assertion = {
    'iss': '{{CLIENT_ID}}',
    'sub': '{{CLIENT_ID}}',
    'aud': '{{TOKEN_ENDPOINT_URL}}',
    'iat': now,
    'exp': now + 600,
    'jti': str(uuid.uuid4())
}

headers = {
    'alg': 'PS256',
    'kid': '{{KEY_ID}}'
}

client_assertion = jwt.encode(assertion, signing_key, algorithm='PS256', headers=headers)
print(f'client_assertion: {client_assertion}')

Including in Request

Include these form parameters in your API request:

client_id={{CLIENT_ID}}
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={{SIGNED_CLIENT_ASSERTION_JWT}}

Method 2: tls_client_auth (mTLS)

With tls_client_auth, your transport client certificate (presented during TLS handshake) serves as your authentication credential. The server identifies you by the certificate itself, without additional parameters.

How It Works

  1. Your transport certificate is presented during the TLS/SSL connection
  2. The server verifies the certificate matches a registered client
  3. No additional parameters needed in the request body
  4. The mTLS connection itself proves your identity

Including in Request

Include only the basic client parameter:

client_id={{CLIENT_ID}}

Your mTLS transport certificate is handled at the TLS layer (via --cert and --key in cURL).


Comparison

Aspectprivate_key_jwttls_client_auth
ComplexityRequires JWT generationUses existing mTLS setup
JWT RequiredYes, must sign for each requestNo
Key Material NeededSigning private keyTransport certificate & key
Code Examples NeededYes (JWT libraries)No (standard TLS)
Recommended ForModern applications with JWT supportSimpler implementations, legacy systems
Use in PostmanCan use pre-request scriptsSimpler—just add cert in Settings

Implementation Guide

Step 1: Choose Your Method

  • Use private_key_jwt if you have JWT libraries available and prefer explicit assertion control
  • Use tls_client_auth if you prefer relying on TLS infrastructure

Step 2: Have Required Credentials

  • private_key_jwt: client_id, signing-key.pem, kid (from JWKS)
  • tls_client_auth: client_id, transport-certificate.pem, transport-key.pem

Step 3: Apply to Each Endpoint

Sections of this guide will reference this page when client authentication is required. Follow the steps for your chosen method.

Step 4: Use with cURL

Both methods work with mTLS transport (use --cert and --key flags):

private_key_jwt example:

curl -X POST "{{endpoint}}" \
  --cert /path/to/transport-certificate.pem \
  --key /path/to/transport-key.pem \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id={{CLIENT_ID}}" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
  -d "client_assertion={{JWT}}"

tls_client_auth example:

curl -X POST "{{endpoint}}" \
  --cert /path/to/transport-certificate.pem \
  --key /path/to/transport-key.pem \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id={{CLIENT_ID}}"


Powered by ozoneapi

© 2026 Open Finance Malaysia Developer Portal. All rights reserved.