Testing Keycloak Integrations: Test Realm, REST API, and Robot Framework

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-dev

For 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-dev

Creating 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_id

Pytest 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 None

Robot 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}    403

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

Read more