JWT Kid Authentication

Learn how to authenticate your requests using industry-standard JWT with certificate fingerprint Key IDs.

Authentication Overview

The Xima CCaaS License Provisioning API uses JWT (JSON Web Token) authentication with certificate fingerprint-based Key IDs, following RFC 7515 (JSON Web Signature) and RFC 7517 (JSON Web Key) industry standards. This approach provides strong security for your API requests without requiring traditional mTLS infrastructure.

To authenticate your requests, you'll need to:

  1. Calculate your certificate fingerprint (SHA-1 hash) for the kid claim
  2. Prepare your license payload including required JWT claims
  3. Create a hash of the payload
  4. Build a JWT containing the payload hash and standard RFC 7519 claims
  5. Sign the JWT with your private key using RS256
  6. Include the JWT in the Authorization header following OAuth 2.0 Bearer Token Usage (RFC 6750)

Industry Standard: This authentication system follows the same JWT kid approach used by major cloud providers like Google Cloud, Microsoft Azure, Auth0, and AWS Cognito for certificate-based authentication.

Important: Never hardcode your private keys in client-side code or share them publicly. This example uses a private key for demonstration purposes only.

Authentication Process

1

Calculate Certificate Fingerprint

Calculate the SHA-1 fingerprint of your client certificate to use as the kid (Key ID) claim.


from cryptography import x509
import hashlib

# Load your client certificate
with open("client-cert.crt", "rb") as f:
    cert_data = f.read()

certificate = x509.load_pem_x509_certificate(cert_data)

# Calculate SHA-1 fingerprint of DER encoding
der_bytes = certificate.public_bytes(encoding=x509.Encoding.DER)
kid = hashlib.sha1(der_bytes).hexdigest()

# Optional: Format for display (with colons)
formatted_fingerprint = ':'.join(kid[i:i+2] for i in range(0, len(kid), 2)).upper()
print(f"Certificate fingerprint: {formatted_fingerprint}")
print(f"Kid for JWT: {kid}")

Note: The kid must be the SHA-1 hash of your certificate's DER encoding (without colons). This fingerprint must match a certificate registered with Xima.

2

Prepare License Payload

Create a JSON object containing the license parameters you want to update, including optional AI messaging, AI audio services, EHR/EMR integration seats, and SIP credentials.


# Create the license payload with ccId, aiMessaging, aiAudioServices, ehrEmrIntegrationSeats, and sipCredentials objects
license_payload = {
    "ccId": {
        "resellerId": "RESELLER001",
        "resellerName": "Acme Distribution",
        "tenantId": "TENANT456",
        "tenantName": "West Coast Sales"
    },
    "aiMessaging": {
        "standardBundles": 5,
        "advancedBundles": 0
    },
    "aiAudioServices": {
        "additionalHours": 25
    },
    "ehrEmrIntegrationSeats": 1,
    "digitalOnlySeats": 15,
    "essentialSeats": 0,
    "professionalSeats": 25,
    "eliteSeats": 10,
    "workforceOptimizationSeats": 10,
    "speechAnalyticsSeats": 5,
    "dialerSeats": 0,
    "sipCredentials": {
        "username": "user123",
        "password": "secret",
        "sipPort": 5061,
        "sipServer": "sip_server.example.com",
        "transport": "udp",
        "outboundProxy": "outbound.example.com"
    }
}
3

Hash the Payload

Serialize the JSON with specific parameters and calculate its SHA-256 hash.

import json
import hashlib

# Serialize JSON with specific parameters
payload_string = json.dumps(license_payload, sort_keys=False, separators=(',', ':')).encode('utf-8')

# Calculate SHA-256 hash
payload_hash = hashlib.sha256(payload_string).hexdigest()

Note: The serialization parameters are critical. You must use sort_keys=False and separators=(',', ':') for the server to calculate the same hash.

4

Create JWT with RFC 7519 Claims

Build the JWT header and payload with standard RFC claims and certificate fingerprint.

import base64
import uuid
import time

# Generate a unique JTI
jti = str(uuid.uuid4())

# Set timestamps
current_time = int(time.time())
expiration_time = current_time + (30 * 60)  # 30 minutes in the future

# JWT Header with certificate fingerprint
jwt_header = {
    "alg": "RS256",
    "typ": "JWT",
    "kid": kid  # Certificate fingerprint from Step 1
}

# JWT Payload with RFC 7519 standard claims
jwt_payload = {
    "iss": "xima-ccaas",           # Issuer (required)
    "sub": kid,                    # Subject - certificate fingerprint
    "aud": "xima-ccaas",           # Audience (required)
    "payload_hash": payload_hash,   # Request body hash
    "jti": jti,                    # JWT ID - unique identifier
    "exp": expiration_time,        # Expiration time
    "iat": current_time            # Issued at time
}

# Base64URL encoding function
def base64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')

# Encode header and payload
header_encoded = base64url_encode(json.dumps(jwt_header).encode('utf-8'))
payload_encoded = base64url_encode(json.dumps(jwt_payload).encode('utf-8'))

RFC 7519 Claims: The issuer (iss) and audience (aud) must be "xima-ccaas". The subject (sub) should be your certificate fingerprint. Each JTI must be unique to prevent replay attacks.

5

Sign the JWT with RS256

Sign the encoded header and payload with your private key using RS256 algorithm.

from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes

# Load your private key (matching the certificate)
from cryptography.hazmat.primitives.serialization import load_pem_private_key
with open("client-key.pem", "rb") as f:
    private_key = load_pem_private_key(f.read(), password=None)

# Create message to sign
message = f"{header_encoded}.{payload_encoded}".encode('utf-8')

# Sign with RS256 (PKCS#1 v1.5 padding + SHA-256)
signature = private_key.sign(
    message,
    padding.PKCS1v15(),
    hashes.SHA256()
)

# Encode the signature
signature_encoded = base64url_encode(signature)

# Combine to form complete JWT
jwt = f"{header_encoded}.{payload_encoded}.{signature_encoded}"

RS256 Algorithm: Uses PKCS#1 v1.5 padding with SHA-256 hashing, following RFC 7518 standards for JWT signatures.

6

Send Authenticated Request

Make the API request with the JWT in the Authorization header following OAuth 2.0 Bearer Token Usage (RFC 6750).

import requests

# API endpoint
api_url = "https://api.ximadev.cloud/v1/licensing/update"

# Headers with JWT Bearer token (RFC 6750)
headers = {
    "Authorization": f"Bearer {jwt}",
    "Content-Type": "application/json"
}

# Send request with serialized payload
response = requests.post(
    api_url,
    headers=headers,
    data=payload_string  # This is the serialized license payload
)

# Process response
if response.status_code == 200:
    print("Success!")
    print(response.json())
else:
    print(f"Error: {response.status_code}")
    print(response.text)

OAuth 2.0 Bearer: The Authorization header follows RFC 6750 Bearer Token Usage standard for transmitting JWTs in HTTP requests.

AI Messaging & AI Audio Services

AI Messaging: Configure billable AI messaging by specifying the number of additionalMessageBundles (each bundle = 1000 messages).
AI Audio Services: Configure by specifying additionalHours (number of additional AI transcription hours).

EHR/EMR Integration: Use ehrEmrIntegrationSeats to specify the number of agent users with EHR/EMR integration.

Professional and Elite seats include a base number of AI messages and AI audio hours per agent. The aiMessaging.additionalMessageBundles and aiAudioServices.additionalHours fields are for purchasing additional capacity beyond the included base. Each message bundle = 1000 messages.

If SIP registration is required, include the optional sipCredentials object with the following properties: username, password, sipServer, outboundProxy (all required). sipPort and transport are optional but must be provided together - when omitted, SRV lookup is used.

{
  "ccId": {
    "resellerId": "RESELLER001",
    "resellerName": "Acme Distribution",
    "divisionId": "TENANT456",
    "divisionName": "West Coast Sales"
  },
  "aiMessaging": {
    "standardBundles": 5,
    "advancedBundles": 0
  },
  "aiAudioServices": {
    "additionalHours": 20
  },
  "sipCredentials": {
    "username": "user123",
    "password": "secret",
    "sipServer": "sip_server.example.com",
    "sipPort": 5061,
    "transport": "udp",
    "outboundProxy": "outbound.example.com"
  }
}

Complete Code Example

Here's a complete Python example that puts everything together into a reusable function.

import json
import hashlib
import uuid
import time
import base64
import requests
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.serialization import load_pem_private_key

def update_license(certificate_path, private_key_pem, license_data, api_url, expiration_minutes=30):
    """
    Update license using the Xima CCaaS API with JWT kid authentication (RFC 7515/7517)

    Args:
        certificate_path (str): Path to client certificate file (.crt)
        private_key_pem (str): PEM-encoded private key
        license_data (dict): License payload
        api_url (str): API endpoint URL
        expiration_minutes (int): Minutes until expiration (default: 30, max: 30)

    Returns:
        dict: API response data
    """
    from cryptography import x509
    
    # Step 1: Calculate certificate fingerprint for kid
    with open(certificate_path, "rb") as f:
        cert_data = f.read()
    
    certificate = x509.load_pem_x509_certificate(cert_data)
    der_bytes = certificate.public_bytes(encoding=x509.Encoding.DER)
    kid = hashlib.sha1(der_bytes).hexdigest()

    # Step 2: Load private key
    private_key = load_pem_private_key(
        private_key_pem.encode('utf-8'),
        password=None
    )

    # Ensure expiration is within limits
    expiration_minutes = min(expiration_minutes, 30)

    # Step 3: Calculate timestamps
    current_time = int(time.time())
    expiration_time = current_time + (expiration_minutes * 60)

    # Generate unique JTI
    jti = str(uuid.uuid4())

    # Step 4: Serialize payload with specific parameters
    payload_string = json.dumps(license_data, sort_keys=False, separators=(',', ':')).encode('utf-8')
    payload_hash = hashlib.sha256(payload_string).hexdigest()

    # Step 5: Base64URL encode function
    def base64url_encode(data):
        return base64.urlsafe_b64encode(data).rstrip(b'=').decode('utf-8')

    # Step 6: Create JWT header and payload with RFC 7519 claims
    jwt_header = {
        "alg": "RS256",
        "typ": "JWT",
        "kid": kid  # Certificate fingerprint
    }

    # JWT payload with standard RFC claims
    jwt_payload = {
        "iss": "xima-ccaas",        # Issuer (required)
        "sub": kid,                 # Subject - certificate fingerprint
        "aud": "xima-ccaas",        # Audience (required)
        "payload_hash": payload_hash,
        "jti": jti,                 # JWT ID - unique identifier
        "exp": expiration_time,     # Expiration time
        "iat": current_time         # Issued at time
    }

    # Step 7: Encode header and payload
    header_encoded = base64url_encode(json.dumps(jwt_header).encode('utf-8'))
    payload_encoded = base64url_encode(json.dumps(jwt_payload).encode('utf-8'))

    # Step 8: Sign the JWT with RS256
    message = f"{header_encoded}.{payload_encoded}".encode('utf-8')
    signature = private_key.sign(
        message,
        padding.PKCS1v15(),
        hashes.SHA256()
    )
    signature_encoded = base64url_encode(signature)

    # Step 9: Build complete JWT
    jwt = f"{header_encoded}.{payload_encoded}.{signature_encoded}"

    # Step 10: Send authenticated request (RFC 6750 Bearer Token)
    headers = {
        "Authorization": f"Bearer {jwt}",
        "Content-Type": "application/json"
    }

    response = requests.post(
        api_url,
        headers=headers,
        data=payload_string
    )

    # Step 11: Handle response
    if response.status_code == 200:
        return response.json()
    else:
        raise Exception(f"API Error ({response.status_code}): {response.text}")

Common Issues & Troubleshooting

Invalid Signature Error

If you receive an INVALID_SIGNATURE error, check the following:

  • Ensure you're using the correct serialization parameters (sort_keys=False, separators=(',', ':'))
  • Verify you're using the same exact serialized payload string for both hashing and the request body
  • Confirm you're using the correct private key (matching the certificate used in the CSR process)
  • Make sure the kid in your JWT header matches the one provided by Xima

Expired or Invalid Token

JWT tokens have time-based validation. If you receive an INVALID_TOKEN error, check:

  • Your server's clock is synchronized (NTP)
  • You're generating a new jti and exp for each request

Validation Errors

The API enforces certain business rules for license configurations:

  • workforceOptimizationSeats and workforceManagementSeats require at least 10 seats
  • speechAnalyticsSeats and dialerSeats require at least 5 seats
  • additionalAiMessages requires at least one eliteSeats

Next Steps

Now that you understand how to authenticate your API requests, you can:

API Reference

Explore the full API reference documentation to understand all available endpoints and parameters.

Interactive Examples

Try the interactive examples in Google Colab to see the full authentication flow, including CSR generation and certificate handling.