Skip to main content

Private Key JWT

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 Users, see our dedicated guide here.

Register an Application & Generate a Key Pair​

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.

  1. In your ZITADEL project, click New to add an application.
Register the app
  1. Enter a name (e.g., "Backend Client") and select API as the application type.
Set application type
  1. Choose JWT as the authentication method and click Continue.
Choose JWT Profile
  1. Review your settings and click Create.
Create application
  1. After creation, you’ll see the application’s Client ID. There is no client secret—instead, authentication relies on your private key and JWT.
Client ID
  1. To generate a key pair, click New under the application’s keys section.
Generate a new key
  1. Select JSON as the key type, set an expiration if desired, and click Add.
Key details
  1. Download and save the generated key by clicking Download. Afterward, click Close.
Download key
  1. 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>"
}
  1. In the left menu, select URLs to view the application’s OIDC endpoints. Note the issuer URL, token_endpoint, and introspection_endpoint.
OIDC Endpoints
  1. Optionally, note your Resource ID for reference.
Resource ID

Calling the Introspection Endpoint​

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:

ParameterDescription
client_assertionThe JWT assertion created and signed as described below.
client_assertion_typeMust be urn:ietf:params:oauth:client-assertion-type:jwt-bearer
tokenThe access token you want to introspect.

Creating the Client Assertion (JWT)​

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/.

Example Introspection HTTP Request​

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

Python Example​

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

Introspection Response​

Upon successful introspection, regardless of the token type or introspection method, a response with the boolean active is returned, indicating if the provided token is active and if the requesting client is part of the token audience. If active is true, further information will be provided:

PropertyDescription
audThe audience of the token
client_idThe client_id of the application the token was issued to
expTime the token expires (as unix time)
iatTime the token was issued at (as unix time)
issIssuer of the token
jtiUnique id of the token
nbfTime the token must not be used before (as unix time)
scopeSpace delimited list of scopes granted to the token
token_typeType of the inspected token. Value is always Bearer
usernameZITADEL's login name of the user. Consists of username@primarydomain

Depending on the granted scopes, additional information about the authorized user is provided.

If the authorization fails, an HTTP 401 with invalid_client will be returned.

In summary, the introspection endpoint plays a crucial role in validating access tokens, either opaque or JWT, ensuring that they are not revoked.

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.

Was this page useful?