Configure private key JWT authentication for service users
This guide demonstrates how developers can leverage private key JWT authentication to secure communication between service users and client applications within ZITADEL.
In ZITADEL we use the urn:ietf:params:oauth:grant-type:jwt-bearer
(“JWT bearer token with private key”, RFC7523) authorization grant for this non-interactive authentication.
Read more about the different authentication methods for service users and their benefits, drawbacks, and security considerations.
How private key JWT authentication works
- Generate a private/public key pair associated with the service user.
- The authorization server stores the public key; and
- returns the private key as a json file
- The developer configures the client in such a way, that
- JWT assertion is created with the subject of the service user, and the JWT is signed by the private key
- Resource owner requests a token by sending the client_assertion
- Authorization server validates the signature using the service user's public key
- Authorization server returns an OAuth access_token
- Resource Owner calls a Resource Server by including the access_token in the Header
- Resource Server validates the JWT with token introspection
Prerequisites
A code library/framework supporting JWT generation and verification (e.g., pyjwt
for Python, jsonwebtoken
for Node.js).
Steps to authenticate a Service User with private JWT
You need to follow these steps to authenticate a service user and receive an access token that can be used in subsequent requests.
1. Create a Service User
- Navigate to Service Users
- Click on New
- Enter a username and a display name
- Click on Create
2. Generate a private key file
- Access the ZITADEL web console and navigate to the service user details.
- Click on the Keys menu point in the detail of your new service user
- Click on New
- You can either set an expiration date or leave it empty if you don't want it to expire
- Click on Download and save the key file
Make sure to save the key file. You won't be able to retrieve it again. If you lose it, you will have to generate a new one.
If you specify an expiration date, note that the key will expire at midnight that day
The downloaded JSON should look something like outlined below. The value of key
contains the private key for your service account. Please make sure to keep this key securely stored and handled with care. The public key is automatically stored in ZITADEL.
{
"type":"serviceaccount",
"keyId":"100509901696068329",
"key":"-----BEGIN RSA PRIVATE KEY----- [...] -----END RSA PRIVATE KEY-----\n",
"userId":"100507859606888466"
}
3. Create a JWT and sign with private key
You need to create a JWT with the following header and payload and sign it with the RS256 algorithm.
Header
{
"alg": "RS256",
"kid":"100509901696068329"
}
Make sure to include kid
in the header with the value of keyId
from the downloaded JSON.
Payload
{
"iss": "100507859606888466",
"sub": "100507859606888466",
"aud": "https://$CUSTOM-DOMAIN",
"iat": [Current UTC timestamp, e.g. 1605179982, max. 1 hour ago],
"exp": [UTC timestamp, e.g. 1605183582]
}
iss
represents the requesting party, i.e. the owner of the private key. In this case the value ofuserId
from the downloaded JSON.sub
represents the application. Set the value also to the value ofuserId
aud
must be your Custom Domainiat
is a unix timestamp of the creation signing time of the JWT, e.g. now and must not be older than 1 hour agoexp
is the unix timestamp of expiry of this assertion
Please refer to JWT with private key API reference for further information.
If you use Go, you might want to use the provided tool to generate a JWT from the downloaded JSON. There are many libraries to generate and sign JWT.
Code Example (Python using pyjwt
):
import jwt
import datetime
# Replace with your service user ID and private key
service_user_id = "your_service_user_id"
private_key = "-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY\n-----END PRIVATE KEY-----"
key_id = "your_key_id"
# ZITADEL API URL (replace if needed)
api_url = "your_custom_domain"
# Generate JWT claims
payload = {
"iss": service_user_id,
"sub": service_user_id,
"aud": api_url,
"exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=5),
"iat": datetime.datetime.now(datetime.timezone.utc)
}
header = {
"alg": "RS256",
"kid": key_id
}
# Sign the JWT using RS256 algorithm
encoded_jwt = jwt.encode(payload, private_key, algorithm="RS256", headers=header)
print(f"Generated JWT: {encoded_jwt}")
4. Request an OAuth token with the generated JWT
With the encoded JWT from the prior step, you will need to craft a POST request to ZITADEL's token endpoint:
curl --request POST \
--url https:/$CUSTOM-DOMAIN/oauth/v2/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
--data scope='openid' \
--data assertion=eyJ0eXAiOiJKV1QiL...
grant_type
should be set tourn:ietf:params:oauth:grant-type:jwt-bearer
scope
should contain any Scopes you want to include, but must includeopenid
.assertion
is the encoded value of the JWT that was signed with your private key from the prior step
If you want to access ZITADEL APIs, make sure to include the required scopes urn:zitadel:iam:org:project:id:zitadel:aud
.
Read our guide how to access ZITADEL APIs to learn more.
You should receive a successful response with access_token
, token_type
and time to expiry in seconds as expires_in
.
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "MtjHodGy4zxKylDOhg6kW90WeEQs2q...",
"token_type": "Bearer",
"expires_in": 43199
}
5. Include the access token in the authorization header
When making API requests on behalf of the service user, include the generated token in the "Authorization" header with the "Bearer" prefix.
curl --request POST \
--url $YOUR_API_ENDOINT \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Bearer MtjHodGy4zxKylDOhg6kW90WeEQs2q...'
Accessing ZITADEL APIs
You might want to access ZITADEL APIs to manage resources, such as users, or to validate tokens sent to your backend service. Follow our guides on how to access ZITADEL API to use the ZITADEL APIs with your service user.
Token introspection
Your API endpoint might receive tokens from users and need to validate the token with ZITADEL. In this case your API needs to authenticate with ZITADEL and then do a token introspection. Follow our guide on token introspection with private key JWT to learn more.
Client application authentication
The above steps demonstrate service user authentication. If your application also needs to authenticate itself, you can utilize Client Credentials Grant. Refer to ZITADEL documentation for details on this alternative method.
Security considerations
- Store private keys securely: Never share or embed the private key in your code or application. Consider using secure key management solutions.
- Set appropriate JWT expiration times: Limit the validity period of tokens to minimize the impact of potential compromise.
- Implement proper error handling: Handle situations where JWT verification fails or tokens are expired.
By following these steps and adhering to security best practices, you can effectively secure service user and client application communication within ZITADEL using private key JWT authentication.