Keycloak JWT
This page provides a step-by-step guide for integrating Keycloak as a Single Sign-On (SSO) provider with the Timbr platform. By following this tutorial, you will configure OpenID Connect (OIDC) authentication with secure PKCE (Proof Key for Code Exchange), enable role mapping from Keycloak roles and groups, and allow automatic user registration on first login.
Prerequisites
Before you begin, ensure you have:
- Keycloak server installed and running (version 12.0 or later recommended)
- Supports self-hosted Keycloak or Red Hat Single Sign-On (RH-SSO)
- Administrative access to your Keycloak Admin Console
- Administrative access to the Timbr platform server to configure environment variables
- Keycloak realm created (or use the default
masterrealm) - Your Timbr domain URL (e.g.,
https://timbr.example.com)
Step 1: Create an OIDC Client in Keycloak
1.1 Create or Select a Realm
- Log in to the Keycloak Admin Console (e.g.,
https://keycloak.example.com/auth/admin) - In the top-left dropdown, select an existing realm or click Add realm to create a new one
- Name:
timbr(or your preferred name) - Click Create
- Name:
Step 2: Create a Client for Timbr
- In your realm, navigate to Clients in the left sidebar
- Click Create
- Configure the client:
- Client ID:
timbr-api(or your preferred ID) - Client Protocol:
openid-connect - Root URL: Your Timbr API URL (e.g.,
https://timbr-api.example.com)
- Client ID:
- Click Save
Step 3: Configure Client Settings
After creating the client, configure the following settings:
- Settings tab:
- Access Type:
confidentialorpublic(useconfidentialfor production) - Standard Flow Enabled:
ON(for OAuth authorization code flow) - Direct Access Grants Enabled:
ON(for direct username/password authentication) - Service Accounts Enabled:
ON(optional, for service-to-service authentication) - Valid Redirect URIs: Add your application's redirect URIs
- Web Origins: Add
*or specific origins for CORS
- Access Type:
- Click Save
Step 4: Obtain the Realm Public Key
To validate JWT tokens, Timbr needs the Keycloak realm's public key:
Method 1: From Realm Settings (Recommended)
- Navigate to Realm Settings in the left sidebar
- Click the Keys tab
- Find the row with Algorithm
RS256(or your configured algorithm) - Click the Public key button to view the key
- Copy the public key value
The key will look like:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
Method 2: From OpenID Configuration Endpoint
You can also retrieve the public key from the JWKS (JSON Web Key Set) endpoint:
curl https://keycloak.example.com/auth/realms/timbr/protocol/openid-connect/certs
Look for the key with "use": "sig" and extract the modulus and exponent to construct the public key.
Format the Public Key for Timbr
The public key must be formatted with PEM headers and newlines:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
(key continues)
...
-----END PUBLIC KEY-----
For the Timbr environment variable, format it as a single-line string with \n for newlines:
\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...rest_of_key...\n-----END PUBLIC KEY-----\n
The public key must include the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- markers with \n newline characters in the environment variable format.
Step 5: Create Users (if needed)
- Navigate to Users in the left sidebar
- Click Add user
- Configure the user:
- Username: The username for authentication
- Email: User's email address
- First Name / Last Name: User's name
- Email Verified:
ON(if you want to skip email verification)
- Click Save
- Go to the Credentials tab
- Set a password:
- Password: Enter a password
- Temporary:
OFF(unless you want to force password change on first login)
- Click Set Password
Step 6: Configure Token Settings (Optional)
To customize token expiration and other settings:
- Navigate to Realm Settings > Tokens tab
- Configure:
- Access Token Lifespan: How long access tokens are valid (default: 5 minutes)
- Client Session Idle: Maximum time a session can be idle
- Client Session Max: Maximum session duration
- Click Save
Configure Timbr API Service
Add the JWT authentication configuration to your timbr-api service to enable Keycloak integration.
Required Environment Variables
# Enable JWT token authentication
ENABLE_TOKEN=true
# The Keycloak realm public key (formatted with \n for newlines)
JWT_DEFAULT_KEY=\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...rest_of_key...\n-----END PUBLIC KEY-----\n
Optional Environment Variables
# Algorithm for JWT validation (default: RS246)
JWT_DEFAULT_ALGORITHM=RS256
# JWT audience validation (commonly the Keycloak Client ID)
JWT_DEFAULT_AUDIENCE=timbr-api
# User identification field from JWT token (default: email)
JWT_USE_EMAIL_OR_USER=email
# JWT type (default: custom, use custom for Keycloak)
JWT_TYPE=custom
Single-Tenant Configuration
For a single Keycloak realm, configure the Timbr API service with the realm's public key.
Step 1: Format the Public Key
Take the public key from Keycloak (see Step 4 above) and format it:
Original format:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1234567890abcdef...
...more key data...
...ending key data...
-----END PUBLIC KEY-----
Environment variable format (single line with \n):
\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1234567890abcdef...\n-----END PUBLIC KEY-----\n
Step 2: Configure Environment Variables
Docker Compose
Add to your timbr-api service in docker-compose.yml:
services:
timbr-api:
# ...
environment:
- ENABLE_TOKEN=true
- JWT_DEFAULT_KEY=\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...rest_of_key...\n-----END PUBLIC KEY-----\n
- JWT_DEFAULT_ALGORITHM=RS256
- JWT_DEFAULT_AUDIENCE=timbr-api
- JWT_USE_EMAIL_OR_USER=email
- JWT_TYPE=custom
Restart the service:
sudo docker-compose up -d timbr-api
Kubernetes
Add to your timbr-api Deployment manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: timbr-api
spec:
template:
spec:
containers:
- name: timbr-api
# ...
env:
# ...
- name: ENABLE_TOKEN
value: "true"
- name: JWT_DEFAULT_KEY
value: "\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...rest_of_key...\n-----END PUBLIC KEY-----\n"
- name: JWT_DEFAULT_ALGORITHM
value: "RS256"
- name: JWT_DEFAULT_AUDIENCE
value: "timbr-api"
- name: JWT_USE_EMAIL_OR_USER
value: "email"
- name: JWT_TYPE
value: "custom"
Apply the manifest:
kubectl apply -f timbr-api.yaml
For better security, store the public key in a Kubernetes Secret:
- name: JWT_DEFAULT_KEY
valueFrom:
secretKeyRef:
name: keycloak-public-key
key: public-key
Environment Variables Reference
| Environment Variable | Required | Default Value | Description |
|---|---|---|---|
ENABLE_TOKEN | ✔️ | false | Must be set to true to enable JWT authentication |
JWT_DEFAULT_KEY | ✔️ | None | Keycloak realm public key (formatted with \n for newlines, including PEM headers) |
JWT_DEFAULT_ALGORITHM | ✖️ | RS246 | Algorithm for JWT validation. Use RS256 or RS246 for Keycloak (RSA public key). Multiple algorithms can be comma-separated (e.g., RS246,RSA-OAEP) |
JWT_DEFAULT_AUDIENCE | ✖️ | None | Expected audience in the JWT token (typically the Keycloak Client ID). If not set, audience validation is skipped |
JWT_USE_EMAIL_OR_USER | ✖️ | email | Which claim to use for user identification: email or username |
JWT_TYPE | ✖️ | custom | JWT provider type. Use custom for Keycloak (do not use azure) |
Multi-Tenant Configuration
For multiple Keycloak realms or tenants, configure tenant-specific credentials using environment variables with tenant IDs.
Overview
Multi-tenant configuration allows:
- Different Keycloak realms for different organizations
- Separate public keys for each tenant
- Tenant-specific audience validation
- Prefixing usernames with tenant IDs to avoid conflicts
Step 1: Configure Tenant-Specific Variables
For each tenant, create environment variables with the format JWT_<TENANT_ID>_*, where <TENANT_ID> is an alphanumeric identifier for the tenant.
Example for two tenants: "acme" and "globex"
# Tenant: acme
JWT_ACME_KEY=\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...acme_key...\n-----END PUBLIC KEY-----\n
JWT_ACME_ALGORITHM=RS256
JWT_ACME_AUDIENCE=acme-timbr-client
# Tenant: globex
JWT_GLOBEX_KEY=\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...globex_key...\n-----END PUBLIC KEY-----\n
JWT_GLOBEX_ALGORITHM=RS256
JWT_GLOBEX_AUDIENCE=globex-timbr-client
# Enable tenant user prefixing (optional)
JWT_USE_TENANT_USER=True
Step 2: Deployment Configuration
Docker Compose
services:
timbr-api:
# ...
environment:
- ENABLE_TOKEN=true
- JWT_TYPE=custom
- JWT_USE_EMAIL_OR_USER=email
# Tenant: acme
- JWT_ACME_KEY=\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...acme_key...\n-----END PUBLIC KEY-----\n
- JWT_ACME_ALGORITHM=RS256
- JWT_ACME_AUDIENCE=acme-timbr-client
# Tenant: globex
- JWT_GLOBEX_KEY=\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...globex_key...\n-----END PUBLIC KEY-----\n
- JWT_GLOBEX_ALGORITHM=RS256
- JWT_GLOBEX_AUDIENCE=globex-timbr-client
# Enable tenant user prefixing
- JWT_USE_TENANT_USER=True
Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: timbr-api
spec:
template:
spec:
containers:
- name: timbr-api
env:
- name: ENABLE_TOKEN
value: "true"
- name: JWT_TYPE
value: "custom"
- name: JWT_USE_EMAIL_OR_USER
value: "email"
# Tenant: acme
- name: JWT_ACME_KEY
value: "\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...acme_key...\n-----END PUBLIC KEY-----\n"
- name: JWT_ACME_ALGORITHM
value: "RS256"
- name: JWT_ACME_AUDIENCE
value: "acme-timbr-client"
# Tenant: globex
- name: JWT_GLOBEX_KEY
value: "\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...globex_key...\n-----END PUBLIC KEY-----\n"
- name: JWT_GLOBEX_ALGORITHM
value: "RS256"
- name: JWT_GLOBEX_AUDIENCE
value: "globex-timbr-client"
# Enable tenant user prefixing
- name: JWT_USE_TENANT_USER
value: "True"
Multi-Tenant Environment Variables
| Environment Variable | Required | Default Value | Description |
|---|---|---|---|
JWT_<TENANT_ID>_KEY | ✔️ | None | Tenant-specific public key (formatted with \n for newlines) |
JWT_<TENANT_ID>_ALGORITHM | ✖️ | RS246 | Tenant-specific algorithm for JWT validation |
JWT_<TENANT_ID>_AUDIENCE | ✖️ | None | Tenant-specific audience (Client ID) for validation |
JWT_USE_TENANT_USER | ✖️ | False | When True, prefixes username with tenant ID (format: <tenant-id>/<username>) |
Using Tenant-Specific Configuration
When making API requests with tenant-specific configuration, include the x-jwt-tenant-id header:
curl -X GET https://timbr-api.example.com/api/v1/ontologies \
-H "x-jwt-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6..." \
-H "x-jwt-tenant-id: acme"
This tells Timbr to use the JWT_ACME_* environment variables for token validation.
Username Prefixing with JWT_USE_TENANT_USER
When JWT_USE_TENANT_USER=True, usernames are prefixed with the tenant ID:
Example:
- Tenant ID:
acme - JWT token contains:
"email": "[email protected]" - Timbr authenticates as:
acme/[email protected]
This allows different tenants to have users with the same email/username without conflicts.
Making API Requests
After configuring JWT authentication, use Keycloak access tokens to authenticate API requests to Timbr.
Step 1: Obtain an Access Token from Keycloak
Using Direct Access Grant (Resource Owner Password Credentials)
curl -X POST https://keycloak.example.com/auth/realms/timbr/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=timbr-api" \
-d "client_secret=<your-client-secret>" \
-d "[email protected]" \
-d "password=<user-password>" \
-d "grant_type=password"
Response:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6...",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsInR5cCI6...",
"token_type": "Bearer",
"scope": "email profile"
}
Using Authorization Code Flow (for web applications)
For web applications, implement the OAuth 2.0 authorization code flow:
- Redirect user to Keycloak authorization endpoint
- User authenticates and authorizes the application
- Keycloak redirects back with an authorization code
- Exchange the code for an access token
See Keycloak documentation for details.
Step 2: Make API Requests with the Token
Include the access token in the x-jwt-token header:
Single-Tenant Request
curl -X GET https://timbr-api.example.com/api/v1/ontologies \
-H "x-jwt-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6..."
Multi-Tenant Request
Include both the token and the tenant ID:
curl -X GET https://timbr-api.example.com/api/v1/ontologies \
-H "x-jwt-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6..." \
-H "x-jwt-tenant-id: acme"
Request Headers Reference
| Header Key | Required | Description |
|---|---|---|
x-jwt-token | ✔️ | The JWT access token from Keycloak |
x-jwt-tenant-id | ✖️ | Tenant identifier for multi-tenant environments (matches JWT_<TENANT_ID>_* variables) |
All request headers must be in lowercase (x-jwt-token, not X-JWT-Token).
Example: Python API Client
import requests
# Obtain token from Keycloak
keycloak_url = "https://keycloak.example.com/auth/realms/timbr/protocol/openid-connect/token"
token_response = requests.post(
keycloak_url,
data={
"client_id": "timbr-api",
"client_secret": "<your-client-secret>",
"username": "[email protected]",
"password": "<user-password>",
"grant_type": "password"
}
)
access_token = token_response.json()["access_token"]
# Make API request to Timbr
timbr_api_url = "https://timbr-api.example.com/api/v1/ontologies"
headers = {
"x-jwt-token": access_token
}
response = requests.get(timbr_api_url, headers=headers)
print(response.json())
Troubleshooting
Token Validation Fails
Symptoms:
- API returns 401 Unauthorized
- Error message: "Invalid token" or "Token validation failed"
Solutions:
Verify public key format: Ensure the public key includes PEM headers and
\nnewlines\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----\nCheck algorithm: Keycloak typically uses
RS256. VerifyJWT_DEFAULT_ALGORITHM=RS256Verify token hasn't expired: JWT tokens have an expiration time. Check the
expclaimCheck audience: If
JWT_DEFAULT_AUDIENCEis set, ensure it matches theaudclaim in the tokenInspect the token: Use jwt.io to decode the token and verify its contents
User Not Found
Symptoms:
- Token validates successfully but user authentication fails
- Error message: "User not found"
Solutions:
Check user identification field: Verify
JWT_USE_EMAIL_OR_USERmatches the claim you're using- If set to
email, ensure the JWT contains anemailclaim - If set to
username, ensure the JWT contains ausernameorpreferred_usernameclaim
- If set to
Verify user exists in Timbr: Ensure a user with the same email/username exists in Timbr
Multi-tenant prefixing: If using multi-tenants with
JWT_USE_TENANT_USER=True, ensure users are created with the tenant prefix (e.g.,acme/[email protected])
Wrong Tenant Configuration
Symptoms:
- Multi-tenant setup not working
- Error message: "Tenant configuration not found"
Solutions:
Verify tenant ID: Ensure the
x-jwt-tenant-idheader matches the tenant ID in environment variables- Header:
x-jwt-tenant-id: acme - Variables:
JWT_ACME_KEY,JWT_ACME_ALGORITHM, etc.
- Header:
Check tenant ID format: Tenant IDs should be alphanumeric (no special characters or spaces)
Case sensitivity: Environment variable tenant IDs are case-sensitive and should be UPPERCASE
- Correct:
JWT_ACME_KEY - Incorrect:
JWT_acme_KEY
- Correct:
Public Key Format Issues
Symptoms:
- Error message: "Invalid key format" or "Could not parse public key"
Solutions:
Ensure newlines are escaped: Use
\nin the environment variable:JWT_DEFAULT_KEY=\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----\nInclude PEM headers: The key must have
-----BEGIN PUBLIC KEY-----and-----END PUBLIC KEY-----No extra spaces: Remove any spaces or line breaks from the formatted key
Test key extraction: Verify you extracted the correct key from Keycloak (RS256 key from the Keys tab)
Token Expiration
Symptoms:
- Token works initially but fails after some time
- Error message: "Token has expired"
Solutions:
Implement token refresh: Use the refresh token to obtain a new access token before expiration
Adjust token lifespan: In Keycloak Realm Settings > Tokens, increase the Access Token Lifespan
Handle expiration in client: Implement logic to detect expiration and re-authenticate
Debugging Tips
Enable verbose logging: Check Timbr API logs for detailed error messages
Decode the JWT token: Use jwt.io to inspect token claims and verify:
- Expiration (
expclaim) - Issuer (
issclaim) - Audience (
audclaim) - User identification claim (
email,username, orpreferred_username)
- Expiration (
Test with curl: Use curl commands to isolate issues from application code
Verify Keycloak configuration: Ensure the client is configured correctly with proper access types and protocols