Skip to main content

Keycloak JWT

Keycloak logo

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 master realm)
  • 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

  1. Log in to the Keycloak Admin Console (e.g., https://keycloak.example.com/auth/admin)
  2. 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

Step 2: Create a Client for Timbr

  1. In your realm, navigate to Clients in the left sidebar
  2. Click Create
  3. 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)
  4. Click Save

Step 3: Configure Client Settings

After creating the client, configure the following settings:

  1. Settings tab:
    • Access Type: confidential or public (use confidential for 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
  2. Click Save

Step 4: Obtain the Realm Public Key

To validate JWT tokens, Timbr needs the Keycloak realm's public key:

  1. Navigate to Realm Settings in the left sidebar
  2. Click the Keys tab
  3. Find the row with Algorithm RS256 (or your configured algorithm)
  4. Click the Public key button to view the key
  5. 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
Public Key Format

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)

  1. Navigate to Users in the left sidebar
  2. Click Add user
  3. 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)
  4. Click Save
  5. Go to the Credentials tab
  6. Set a password:
    • Password: Enter a password
    • Temporary: OFF (unless you want to force password change on first login)
  7. Click Set Password

Step 6: Configure Token Settings (Optional)

To customize token expiration and other settings:

  1. Navigate to Realm Settings > Tokens tab
  2. 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
  3. 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
Using Kubernetes Secrets

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 VariableRequiredDefault ValueDescription
ENABLE_TOKEN✔️falseMust be set to true to enable JWT authentication
JWT_DEFAULT_KEY✔️NoneKeycloak realm public key (formatted with \n for newlines, including PEM headers)
JWT_DEFAULT_ALGORITHM✖️RS246Algorithm 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✖️NoneExpected audience in the JWT token (typically the Keycloak Client ID). If not set, audience validation is skipped
JWT_USE_EMAIL_OR_USER✖️emailWhich claim to use for user identification: email or username
JWT_TYPE✖️customJWT 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 VariableRequiredDefault ValueDescription
JWT_<TENANT_ID>_KEY✔️NoneTenant-specific public key (formatted with \n for newlines)
JWT_<TENANT_ID>_ALGORITHM✖️RS246Tenant-specific algorithm for JWT validation
JWT_<TENANT_ID>_AUDIENCE✖️NoneTenant-specific audience (Client ID) for validation
JWT_USE_TENANT_USER✖️FalseWhen 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:

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:

  1. Redirect user to Keycloak authorization endpoint
  2. User authenticates and authorizes the application
  3. Keycloak redirects back with an authorization code
  4. 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 KeyRequiredDescription
x-jwt-token✔️The JWT access token from Keycloak
x-jwt-tenant-id✖️Tenant identifier for multi-tenant environments (matches JWT_<TENANT_ID>_* variables)
Header Case Sensitivity

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:

  1. Verify public key format: Ensure the public key includes PEM headers and \n newlines

    \n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----\n
  2. Check algorithm: Keycloak typically uses RS256. Verify JWT_DEFAULT_ALGORITHM=RS256

  3. Verify token hasn't expired: JWT tokens have an expiration time. Check the exp claim

  4. Check audience: If JWT_DEFAULT_AUDIENCE is set, ensure it matches the aud claim in the token

  5. Inspect 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:

  1. Check user identification field: Verify JWT_USE_EMAIL_OR_USER matches the claim you're using

    • If set to email, ensure the JWT contains an email claim
    • If set to username, ensure the JWT contains a username or preferred_username claim
  2. Verify user exists in Timbr: Ensure a user with the same email/username exists in Timbr

  3. 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:

  1. Verify tenant ID: Ensure the x-jwt-tenant-id header matches the tenant ID in environment variables

    • Header: x-jwt-tenant-id: acme
    • Variables: JWT_ACME_KEY, JWT_ACME_ALGORITHM, etc.
  2. Check tenant ID format: Tenant IDs should be alphanumeric (no special characters or spaces)

  3. Case sensitivity: Environment variable tenant IDs are case-sensitive and should be UPPERCASE

    • Correct: JWT_ACME_KEY
    • Incorrect: JWT_acme_KEY

Public Key Format Issues

Symptoms:

  • Error message: "Invalid key format" or "Could not parse public key"

Solutions:

  1. Ensure newlines are escaped: Use \n in the environment variable:

    JWT_DEFAULT_KEY=\n-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----\n
  2. Include PEM headers: The key must have -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY-----

  3. No extra spaces: Remove any spaces or line breaks from the formatted key

  4. 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:

  1. Implement token refresh: Use the refresh token to obtain a new access token before expiration

  2. Adjust token lifespan: In Keycloak Realm Settings > Tokens, increase the Access Token Lifespan

  3. Handle expiration in client: Implement logic to detect expiration and re-authenticate

Debugging Tips

  1. Enable verbose logging: Check Timbr API logs for detailed error messages

  2. Decode the JWT token: Use jwt.io to inspect token claims and verify:

    • Expiration (exp claim)
    • Issuer (iss claim)
    • Audience (aud claim)
    • User identification claim (email, username, or preferred_username)
  3. Test with curl: Use curl commands to isolate issues from application code

  4. Verify Keycloak configuration: Ensure the client is configured correctly with proper access types and protocols


Additional Resources