apps/docs/content/guides/integrate/token-introspection/private-key-jwt.mdx
import IntrospectionResponse from './_introspection-response.mdx';
This guide explains how to use ZITADEL's Private Key JWT authentication method to authenticate your application and call the token introspection endpoint. This is not about securing an API, but about allowing your backend to introspect access tokens in a secure way.
If you're looking to set up Private Key JWT authentication specifically for Service Accounts, see our dedicated guide here.
To use Private Key JWT for introspection, you must register your client application in ZITADEL and create a key pair. This process enables your application to obtain credentials needed to generate JWT assertions for authentication.
In your ZITADEL project, click New to add an application.
Enter a name (e.g., "Backend Client") and select API as the application type.
Choose JWT as the authentication method and click Continue.
Review your settings and click Create.
After creation, you’ll see the application’s Client ID. There is no client secret—instead, authentication relies on your private key and JWT.
To generate a key pair, click New under the application’s keys section.
Select JSON as the key type, set an expiration if desired, and click Add.
Download and save the generated key by clicking Download. Afterward, click Close.
The downloaded key file will look like this:
{
"type": "application",
"keyId": "<YOUR_KEY_ID>",
"key": "-----BEGIN RSA PRIVATE KEY-----\n<YOUR_PRIVATE_KEY>\n-----END RSA PRIVATE KEY-----\n",
"appId": "<YOUR_APP_ID>",
"clientId": "<YOUR_CLIENT_ID>"
}
In the left menu, select URLs to view the application’s OIDC endpoints. Note the issuer URL, token_endpoint, and introspection_endpoint.
Optionally, note your Project ID for reference.
To introspect an access token in ZITADEL, your backend must authenticate by providing a JWT (client_assertion) signed with your application's private key. ZITADEL verifies this JWT to ensure the request is from a trusted application.
Required request parameters:
| Parameter | Description |
|---|---|
client_assertion | The JWT assertion created and signed as described below. |
client_assertion_type | Must be urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
token | The access token you want to introspect. |
The client_assertion parameter is a signed JWT with the following structure:
Header:
{
"alg": "RS256",
"kid": "<YOUR_KEY_ID>"
}
Payload:
{
"iss": "<YOUR_CLIENT_ID>",
"sub": "<YOUR_CLIENT_ID>",
"aud": "https://${YOUR_DOMAIN}", // Your ZITADEL issuer URL
"exp": 1605183582, // Expiry (Unix timestamp)
"iat": 1605179982 // Issued at (Unix timestamp, not older than 1 hour)
}
iss and sub: Your application’s Client ID.aud: The issuer URL for your ZITADEL instance (e.g. https://my-tenant.zitadel.cloud).exp: JWT expiration time (seconds since epoch).iat: JWT issued at time (seconds since epoch), must not be older than 1 hour.You can generate the signed JWT using libraries in your preferred language or helper tools like zitadel-tools or https://dinochiesa.github.io/jwt/.
Make a POST request to the introspection endpoint, sending the above parameters:
curl --request POST \
--url ${YOUR_DOMAIN}/oauth/v2/introspect \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
--data client_assertion=eyJhbGciOiJSUzI1Ni... \
--data token=VjVxyCZmRmWYqd3_F5db9Pb9mHR
Below is a Python function that generates the JWT assertion and performs token introspection:
def introspect_token(token_string):
# Build JWT for client assertion
payload = {
"iss": API_KEY_FILE["clientId"],
"sub": API_KEY_FILE["clientId"],
"aud": ZITADEL_ISSUER_URL,
"exp": int(time.time()) + 60 * 60, # expires in 1 hour
"iat": int(time.time())
}
headers = {
"alg": "RS256",
"kid": API_KEY_FILE["keyId"]
}
jwt_token = jwt.encode(payload, API_KEY_FILE["key"], algorithm="RS256", headers=headers)
req_headers = {"Content-Type": "application/x-www-form-urlencoded"}
req_data = {
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": jwt_token,
"token": token_string
}
response = requests.post(ZITADEL_INTROSPECTION_ENDPOINT, headers=req_headers, data=req_data)
response.raise_for_status()
token_data = response.json()
print(f"Token data from introspection: {token_data}")
return token_data
For more details and a complete working example, see this tutorial on registering an application in ZITADEL and calling the introspection endpoint with a JWT assertion.