Testing Keycloak Integrations: Test Realm, REST API, and Robot Framework
Keycloak testing requires a dedicated test realm to isolate test users and configuration from production. The Keycloak REST Admin API handles test setup and teardown — creating users, resetting passwords, and managing roles programmatically. For E2E tests, token generation via the Direct Access Grant bypasses browser login. This guide covers Keycloak test setup, API-level testing, and Robot Framework integration.
Key Takeaways
Use a dedicated test realm, not the master realm. Create a test realm for automated tests. The master realm has elevated privileges — running test operations there risks configuration changes that affect Keycloak itself.
Keycloak Admin REST API is your setup/teardown tool. Create test users, assign roles, and clean up after tests with the Admin API. This gives you repeatable, isolated test state without manual UI configuration.
Direct Access Grant (Resource Owner Password Grant) for test token generation. Enable this flow on your test client to generate access tokens without browser interaction. Use it in CI where there's no browser available.
Keycloak's Docker image is the right local development setup. Run Keycloak in Docker for local testing with a known configuration. Don't rely on a shared development Keycloak instance — test environments need to be reproducible.
Robot Framework has Keycloak-specific keywords available. The KeycloakLibrary or raw RequestsLibrary calls work for functional testing. Combine with Keycloak's token endpoint for authenticated Robot tests.
Keycloak Docker Setup for Testing
Run Keycloak locally using Docker with a fixed admin password:
# Start Keycloak in dev mode
docker run -d \
--name keycloak-test \
-p 8180:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:24.0 \
start-devFor CI, use a docker-compose or a GitHub Actions service:
# .github/workflows/keycloak-tests.yml
services:
keycloak:
image: quay.io/keycloak/keycloak:24.0
ports:
- "8180:8080"
env:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
options: >-
--health-cmd "curl -f http://localhost:8080/health/ready"
--health-interval 10s
--health-timeout 5s
--health-retries 10
command: start-devCreating a Test Realm via REST API
# keycloak_test_setup.py
import requests
KC_BASE = "http://localhost:8180"
ADMIN_USER = "admin"
ADMIN_PASSWORD = "admin"
TEST_REALM = "test-realm"
def get_admin_token():
response = requests.post(
f"{KC_BASE}/realms/master/protocol/openid-connect/token",
data={
"grant_type": "password",
"client_id": "admin-cli",
"username": ADMIN_USER,
"password": ADMIN_PASSWORD,
},
)
response.raise_for_status()
return response.json()["access_token"]
def create_test_realm(admin_token: str):
"""Create a test realm with standard settings."""
response = requests.post(
f"{KC_BASE}/admin/realms",
headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"},
json={
"realm": TEST_REALM,
"enabled": True,
"displayName": "Test Realm",
"registrationAllowed": False,
"accessTokenLifespan": 300,
},
)
if response.status_code == 409:
print(f"Realm {TEST_REALM} already exists")
else:
response.raise_for_status()
def create_test_client(admin_token: str, client_id: str = "test-app"):
"""Create a test client with Direct Access Grant enabled."""
response = requests.post(
f"{KC_BASE}/admin/realms/{TEST_REALM}/clients",
headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"},
json={
"clientId": client_id,
"enabled": True,
"publicClient": True, # No client secret for public clients
"directAccessGrantsEnabled": True, # Enable Resource Owner Password Grant
"standardFlowEnabled": True,
"redirectUris": ["http://localhost:3000/*", "http://localhost:8080/*"],
"webOrigins": ["*"],
},
)
response.raise_for_status()
def create_test_user(admin_token: str, username: str, password: str, email: str, roles: list = None):
"""Create a test user with a known password."""
user_response = requests.post(
f"{KC_BASE}/admin/realms/{TEST_REALM}/users",
headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"},
json={
"username": username,
"email": email,
"enabled": True,
"emailVerified": True,
"credentials": [{
"type": "password",
"value": password,
"temporary": False,
}],
},
)
user_response.raise_for_status()
user_id = user_response.headers["Location"].split("/")[-1]
# Assign realm roles if specified
if roles:
for role_name in roles:
# Get role
role_response = requests.get(
f"{KC_BASE}/admin/realms/{TEST_REALM}/roles/{role_name}",
headers={"Authorization": f"Bearer {admin_token}"},
)
if role_response.status_code == 200:
requests.post(
f"{KC_BASE}/admin/realms/{TEST_REALM}/users/{user_id}/role-mappings/realm",
headers={"Authorization": f"Bearer {admin_token}", "Content-Type": "application/json"},
json=[role_response.json()],
)
return user_idPytest Fixtures for Keycloak
# conftest.py
import pytest
@pytest.fixture(scope="session", autouse=True)
def keycloak_setup():
"""Set up Keycloak test realm and users once per test session."""
admin_token = get_admin_token()
create_test_realm(admin_token)
create_test_client(admin_token, "test-app")
# Create test roles
create_test_role(admin_token, "user")
create_test_role(admin_token, "admin")
# Create test users
create_test_user(admin_token, "testuser", "Test@123!", "testuser@example.com", ["user"])
create_test_user(admin_token, "testadmin", "Admin@123!", "testadmin@example.com", ["admin"])
yield
# Cleanup (optional — realm delete removes everything)
cleanup_token = get_admin_token()
requests.delete(
f"{KC_BASE}/admin/realms/{TEST_REALM}",
headers={"Authorization": f"Bearer {cleanup_token}"},
)
@pytest.fixture(scope="session")
def user_token():
"""Get a valid token for the test user."""
response = requests.post(
f"{KC_BASE}/realms/{TEST_REALM}/protocol/openid-connect/token",
data={
"grant_type": "password",
"client_id": "test-app",
"username": "testuser",
"password": "Test@123!",
"scope": "openid profile email",
},
)
response.raise_for_status()
return response.json()["access_token"]
@pytest.fixture(scope="session")
def admin_token_for_api():
response = requests.post(
f"{KC_BASE}/realms/{TEST_REALM}/protocol/openid-connect/token",
data={
"grant_type": "password",
"client_id": "test-app",
"username": "testadmin",
"password": "Admin@123!",
"scope": "openid profile email",
},
)
response.raise_for_status()
return response.json()["access_token"]Testing Your Application's Keycloak Integration
# test_keycloak_integration.py
def test_protected_endpoint_with_keycloak_token(client, user_token):
response = client.get(
"/api/profile",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == 200
profile = response.json()
assert profile["username"] == "testuser"
def test_admin_endpoint_blocked_for_regular_user(client, user_token):
response = client.get(
"/api/admin/users",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == 403
def test_admin_endpoint_accessible_for_admin(client, admin_token_for_api):
response = client.get(
"/api/admin/users",
headers={"Authorization": f"Bearer {admin_token_for_api}"},
)
assert response.status_code == 200
def test_token_introspection(user_token):
"""Verify your app can introspect tokens via Keycloak."""
response = requests.post(
f"{KC_BASE}/realms/{TEST_REALM}/protocol/openid-connect/token/introspect",
data={
"token": user_token,
"client_id": "test-app",
},
)
assert response.status_code == 200
result = response.json()
assert result["active"] is True
assert result["username"] == "testuser"Testing Keycloak Token Exchange
If your app uses Keycloak's token exchange (service-to-service authentication):
def test_service_to_service_token_exchange():
"""Test that a service can exchange its own token for a scoped token."""
# Get service token
service_token_response = requests.post(
f"{KC_BASE}/realms/{TEST_REALM}/protocol/openid-connect/token",
data={
"grant_type": "client_credentials",
"client_id": "service-a",
"client_secret": "service-a-secret",
},
)
service_token = service_token_response.json()["access_token"]
# Exchange for a token scoped to service-b
exchange_response = requests.post(
f"{KC_BASE}/realms/{TEST_REALM}/protocol/openid-connect/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": service_token,
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
"audience": "service-b",
"client_id": "service-a",
"client_secret": "service-a-secret",
},
)
assert exchange_response.status_code == 200
exchanged_token = exchange_response.json()["access_token"]
assert exchanged_token is not NoneRobot Framework Integration
Use Robot Framework with the RequestsLibrary to test Keycloak flows:
*** Settings ***
Library RequestsLibrary
Library Collections
*** Variables ***
${KC_BASE} http://localhost:8180
${TEST_REALM} test-realm
${CLIENT_ID} test-app
${USERNAME} testuser
${PASSWORD} Test@123!
${APP_BASE_URL} http://localhost:3000
*** Keywords ***
Get Keycloak Token
[Documentation] Get access token via Resource Owner Password Grant
&{data}= Create Dictionary
... grant_type=password
... client_id=${CLIENT_ID}
... username=${USERNAME}
... password=${PASSWORD}
... scope=openid profile email
${response}= POST
... ${KC_BASE}/realms/${TEST_REALM}/protocol/openid-connect/token
... data=&{data}
Should Be Equal As Integers ${response.status_code} 200
${token}= Get From Dictionary ${response.json()} access_token
[Return] ${token}
Access Protected Resource With Token
[Arguments] ${token} ${url}
&{headers}= Create Dictionary Authorization=Bearer ${token}
${response}= GET ${url} headers=&{headers}
[Return] ${response}
*** Test Cases ***
Authenticated User Can Access Profile
${token}= Get Keycloak Token
${response}= Access Protected Resource With Token ${token} ${APP_BASE_URL}/api/profile
Should Be Equal As Integers ${response.status_code} 200
${body}= Set Variable ${response.json()}
Should Be Equal ${body}[username] testuser
Unauthenticated Request Returns 401
${response}= GET ${APP_BASE_URL}/api/profile
Should Be Equal As Integers ${response.status_code} 401
Invalid Token Returns 401
&{headers}= Create Dictionary Authorization=Bearer invalid.token.here
${response}= GET ${APP_BASE_URL}/api/profile headers=&{headers}
Should Be Equal As Integers ${response.status_code} 401
Admin Endpoint Blocked For Regular User
${token}= Get Keycloak Token
${response}= Access Protected Resource With Token ${token} ${APP_BASE_URL}/api/admin/users
Should Be Equal As Integers ${response.status_code} 403Testing Keycloak with HelpMeTest
For continuous monitoring of your Keycloak-protected application, you can use HelpMeTest to run Robot Framework tests on a schedule. Store the Keycloak token generation as a test setup step, then verify protected endpoints remain accessible after deployments. This catches regressions where Keycloak configuration changes (realm settings, client scope changes) break authentication silently.
Common Keycloak Testing Issues
Token endpoint returns 401 for password grant: Direct Access Grants must be enabled on the client in Keycloak. Go to Client Settings → Advanced → Authentication flows → check "Direct access grants".
User can't authenticate despite correct password: Email verification may be required. Set emailVerified: true when creating users via API, or disable email verification in the realm settings.
Admin API returns 403: The admin token from admin-cli has access to all realms. Make sure you're requesting the token from master realm, not the test realm.
Tests fail after Keycloak restart: Test realm setup must run before tests. Use session-scoped fixtures that check if the realm exists before creating it.
Summary
Keycloak testing centers on three tools: Docker for a reproducible local instance, the Admin REST API for programmatic test setup and teardown, and the Direct Access Grant for token generation in tests. Create a dedicated test realm, create test users with known passwords, and generate tokens directly rather than going through the browser login flow. Robot Framework tests are a natural fit for testing Keycloak-protected applications — the token generation pattern maps cleanly to Robot's keyword-based setup.