CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

testland/keycloak-tests

Use whenever an app uses self-hosted Keycloak and needs integration tests: a throwaway Keycloak per test class via Testcontainers, realm JSON imports, OIDC discovery, token and introspection endpoints, the admin REST API, password, auth-code, client-credentials and token-exchange flows, and UMA.

Quality

94%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files
name:
keycloak-tests
description:
Use whenever an app uses self-hosted Keycloak and needs integration tests: a throwaway Keycloak per test class via Testcontainers, realm JSON imports, OIDC discovery, token and introspection endpoints, the admin REST API, password, auth-code, client-credentials and token-exchange flows, and UMA.

keycloak-tests

Overview

Tests start their own Keycloak in a container, import a committed realm fixture, check the realm is up, then drive the real OIDC endpoints. A realm holds users, credentials, roles and groups; a client is an application that asks Keycloak to authenticate users (Server Admin Guide).

When to use

  • The app uses self-hosted Keycloak as its identity provider.
  • Tests need an isolated Keycloak per run instead of a shared dev or staging server.
  • A test covers client-credentials, authorization-code + PKCE, password (legacy only), token exchange, UMA permissions, or introspection.

Step 1 - Start Keycloak per run with a pinned tag

# tests/auth/conftest.py
import pytest
import requests
from testcontainers.keycloak import KeycloakContainer

@pytest.fixture(scope="session")
def keycloak():
    kc = KeycloakContainer("quay.io/keycloak/keycloak:26.0") \
        .with_realm_import_file("tests/auth/fixtures/test-realm.json")
    with kc:  # the context manager starts and stops the container
        yield kc

with_realm_import_file() mounts the file at /opt/keycloak/data/import/realm.json and adds --import-realm to the start-dev command; start() waits for /health/ready. The admin user comes from the container (test / test unless passed), not from environment variables pointing at another server (testcontainers-python Keycloak module). The default image is latest, so always pass a tag.

Java (testcontainers-keycloak):

@Container
static KeycloakContainer keycloak =
    new KeycloakContainer("quay.io/keycloak/keycloak:26.0")
        .withRealmImportFile("/test-realm.json");   // classpath resource

Every URL comes from the container (keycloak.get_url()); never a literal localhost:8080, which can hit a different server on a developer machine and nothing on CI.

Step 2 - Commit a sanitised realm fixture

Export the realm (Admin Console: Realm settings → Action → Partial export), then sanitise it with one repeatable command and commit the result:

jq '
  del(.smtpServer, .identityProviders, .identityProviderMappers)
  | del(.components["org.keycloak.keys.KeyProvider"])
  | del(.components["org.keycloak.storage.UserStorageProvider"])
  | (.clients[]? | select(.publicClient != true and .secret? != null) | .secret) = "test-secret"
' realm-export.json > tests/auth/fixtures/test-realm.json

This drops environment-coupled blocks (SMTP relay, external identity providers, LDAP user federation) that an isolated container cannot reach, drops signing keys so the container generates its own, and replaces each confidential client's secret with a fixed test value instead of deleting it (an imported confidential client still needs one). Add local test users with fixed passwords under users if tests log in. Then grep the output for anything that still looks live (secret, password, bindCredential, privateKey) before committing.

Step 3 - Check the realm is really there

Before any flow test, assert the started server serves the imported realm, so a bad import fails here and not as a 404 in every test:

@pytest.fixture(scope="session")
def realm(keycloak):
    base = keycloak.get_url()
    r = requests.get(f"{base}/realms/test/.well-known/openid-configuration", timeout=5)
    assert r.status_code == 200, f"realm 'test' not imported: {r.status_code}"
    disco = r.json()
    assert disco["token_endpoint"] == f"{base}/realms/test/protocol/openid-connect/token"
    assert "introspection_endpoint" in disco
    return disco

If it fails: check the container log for the import line, the realm name inside the JSON, and that the file path exists. Fix the fixture, not the paths: Keycloak 17+ serves /realms/... with no /auth prefix (migrating to Quarkus).

Step 4 - Client-credentials grant

def test_client_credentials(realm):
    r = requests.post(realm["token_endpoint"], data={
        "grant_type": "client_credentials",
        "client_id": "service-account-client",
        "client_secret": "test-secret",
    }, timeout=5)
    assert r.status_code == 200
    body = r.json()
    assert body["token_type"].lower() == "bearer"
    assert body["expires_in"] > 0

Pass the token to the app's protected endpoint and assert 200, and pass no token and an expired or tampered one and assert 401.

The password grant must not be used per RFC 9700 (RFC 9700). Keep an existing legacy password-grant test only while its client still allows it; write new user-login tests with authorization-code + PKCE (Step 6).

Step 5 - Token introspection in the resource server

The resource server must authenticate the introspection call with its own client credentials, not with the caller's token, and refuse the request on any answer other than "active": true (non-200, timeout, connection error). Recipe: references/endpoint-recipes.md.

Step 6 - Authorization-code + PKCE, token exchange, UMA

Each of these has a recipe in references/flow-recipes.md:

  • Authorization code + PKCE for browser and mobile logins: one requests.Session that keeps the login page's cookies, submit the form without following redirects, read code from the Location header (the app's private-scheme callback is never followed), send the same code_verifier in the exchange, then prove the token is the expected user via introspection.
  • Standard token exchange: requester client with "Standard token exchange" switched on.
  • UMA / Authorization Services: uma-ticket grant with response_mode=decision.

For a custom browser flow (for example password-less), import the realm with the flow bound, drive it with Playwright through the login page, and assert the redirect carries a code.

Step 7 - Admin REST API

Use the admin client the container provides (python-keycloak KeycloakAdmin logged in with the container's admin user), and seed realm state from the fixture rather than admin calls wherever possible. Recipe: references/endpoint-recipes.md.

Step 8 - CI

jobs:
  auth-integration:
    runs-on: ubuntu-latest      # Docker daemon available
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install "testcontainers[keycloak]" pytest requests
      - run: pytest tests/auth -v

No services: block and no KC_URL / admin-password secrets: each run starts and owns its server, so two builds cannot see each other's data.

Anti-patterns

Anti-patternWhy it failsFix
Shared dev / staging KeycloakBuilds interfere; people change data under the testPer-run container (Step 1)
latest imageResults change when nobody changed the suitePin a tag; upgrade on purpose
Hard-coded localhost:8080Hits whatever runs thereget_url() (Step 1)
Gitignoring the realm, fetching it at run timeClone + Docker no longer enoughSanitised committed fixture (Step 2)
Realm provisioned by admin calls in fixturesSlow, order-dependentImport from JSON (Step 2)
Middleware falls back to local decode when introspection failsAdmits tokens the server did not vouch forRefuse on anything but active: true (Step 5)

Limitations

  • Pin the image and re-run the suite when upgrading; endpoints and admin APIs change between major versions.
  • Custom auth flows need browser automation for full coverage.
  • Token-issuance throughput is out of scope; use k6-load-testing.

References

Workspace
testland
Visibility
Public
Created
Last updated
Source
GitHub Actions
Repository
https://github.com/testland/qaVerified
Badge
testland/keycloak-tests badge