Testing Keycloak: Automated Validation with Testcontainers, Postman, and Cypress

Guilliano Molaire Guilliano Molaire Updated March 16, 2026 12 min read

Last updated: March 2026


Introduction: Why Testing Identity Infrastructure Matters

Identity infrastructure is the front door to every application you build. When authentication breaks, users cannot log in. When authorization fails silently, sensitive data leaks. Despite these stakes, identity systems are routinely the least-tested component in a software stack.

The reasons are understandable. Keycloak is a complex, stateful service that depends on databases, external identity providers, and browser-based OAuth flows. Setting up a realistic test environment has traditionally meant maintaining a shared Keycloak instance that drifts from configuration baselines, accumulates stale test data, and creates flaky tests that everyone learns to ignore.

Modern tooling has changed this. With Testcontainers, you can spin up a fully configured Keycloak instance inside a JUnit test and tear it down when the test finishes. With Postman and Newman, you can validate every step of an OIDC flow against a real Keycloak server and run those validations in CI. With Cypress, you can test the actual login experience your users see, including redirects, SSO, and logout.

This guide walks through all three approaches and shows you how to combine them into a testing strategy that catches configuration errors, permission mistakes, and integration failures before they reach production.

Testcontainers for Java Integration Tests

What Is Testcontainers?

Testcontainers is a Java library that manages disposable Docker containers during test execution. Instead of mocking Keycloak or pointing tests at a shared development instance, you get a real Keycloak server that starts before your test suite and stops after it completes. Each test run gets a clean environment with no state leaking between runs.

This approach eliminates the most common source of test flakiness in identity testing: stale or conflicting test data in a shared instance.

Setting Up a Keycloak Testcontainer

The Keycloak project maintains an official Testcontainers module. Add it to your Maven or Gradle project alongside the Testcontainers JUnit 5 integration:

<dependency>
    <groupId>com.github.dasniko</groupId>
    <artifactId>testcontainers-keycloak</artifactId>
    <version>3.5.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.20.4</version>
    <scope>test</scope>
</dependency>

With the dependencies in place, you can declare a Keycloak container that imports a pre-configured realm:

@Testcontainers
class KeycloakIntegrationTest {

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

    static Keycloak adminClient;

    @BeforeAll
    static void setup() {
        adminClient = keycloak.getKeycloakAdminClient();
    }
}

The test-realm.json file lives in your src/test/resources directory. Export it from a working Keycloak instance using the Admin Console or the Admin CLI, then trim it down to only the clients, roles, and users your tests require.

Writing Integration Tests

Token issuance and claim validation. The most fundamental test verifies that your client configuration produces tokens with the expected claims:

@Test
void shouldIssueTokenWithExpectedClaims() {
    String token = getAccessToken("test-client", "test-user", "password");

    DecodedJWT decoded = JWT.decode(token);

    assertThat(decoded.getIssuer())
        .contains("/realms/test-realm");
    assertThat(decoded.getClaim("preferred_username").asString())
        .isEqualTo("test-user");
    assertThat(decoded.getClaim("realm_access").asMap())
        .containsKey("roles");
}

private String getAccessToken(String clientId, String username, String password) {
    return given()
        .contentType("application/x-www-form-urlencoded")
        .formParam("grant_type", "password")
        .formParam("client_id", clientId)
        .formParam("username", username)
        .formParam("password", password)
        .post(keycloak.getAuthServerUrl() + "/realms/test-realm/protocol/openid-connect/token")
        .then()
        .statusCode(200)
        .extract()
        .path("access_token");
}

You can use the JWT Token Analyzer to manually inspect tokens during development and confirm the claims structure your tests should assert.

User creation via the Admin API. Test that your application can programmatically create users with the correct attributes:

@Test
void shouldCreateUserWithAttributes() {
    UserRepresentation user = new UserRepresentation();
    user.setUsername("new-user");
    user.setEmail("[email protected]");
    user.setEnabled(true);
    user.setAttributes(Map.of("department", List.of("engineering")));

    Response response = adminClient.realm("test-realm").users().create(user);

    assertThat(response.getStatus()).isEqualTo(201);

    List<UserRepresentation> users = adminClient.realm("test-realm")
        .users().searchByUsername("new-user", true);
    assertThat(users).hasSize(1);
    assertThat(users.get(0).getAttributes().get("department"))
        .containsExactly("engineering");
}

Role assignment and token claims. Verify that role assignments propagate correctly into access tokens:

@Test
void shouldIncludeAssignedRolesInToken() {
    String userId = adminClient.realm("test-realm")
        .users().searchByUsername("test-user", true)
        .get(0).getId();

    RoleRepresentation adminRole = adminClient.realm("test-realm")
        .roles().get("app-admin").toRepresentation();

    adminClient.realm("test-realm")
        .users().get(userId)
        .roles().realmLevel()
        .add(List.of(adminRole));

    String token = getAccessToken("test-client", "test-user", "password");
    DecodedJWT decoded = JWT.decode(token);

    List<String> roles = decoded.getClaim("realm_access")
        .as(RealmAccess.class).getRoles();
    assertThat(roles).contains("app-admin");
}

CI/CD Integration with GitHub Actions

Running Testcontainers in CI requires Docker access. GitHub Actions hosted runners include Docker by default, so no special configuration is needed:

jobs:
  keycloak-integration-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Run integration tests
        run: ./mvnw verify -Pintegration-tests

Testcontainers detects the CI environment automatically and adjusts container startup timeouts. For faster feedback, consider using Testcontainers Cloud, which runs containers on remote infrastructure and avoids the overhead of pulling images on every CI run.

Postman and Newman for OIDC Flow Testing

While Testcontainers focuses on API-level testing from Java, Postman collections let you validate the complete OIDC protocol flows that your applications depend on. This is especially useful for teams where the identity configuration is managed separately from the application code.

Testing the Authorization Code Flow

Create a Postman collection that walks through each step of the Authorization Code flow. Each request builds on the previous one using Postman’s variable chaining:

Step 1: Discover endpoints. Hit the well-known configuration endpoint and extract the authorization, token, and revocation URLs:

GET {{keycloak_url}}/realms/{{realm}}/.well-known/openid-configuration

Save the response values as collection variables in a test script:

const config = pm.response.json();
pm.collectionVariables.set("authorization_endpoint", config.authorization_endpoint);
pm.collectionVariables.set("token_endpoint", config.token_endpoint);
pm.collectionVariables.set("revocation_endpoint", config.revocation_endpoint);

Step 2: Exchange credentials for tokens. For testing purposes, use the direct access grant (resource owner password) to obtain tokens without browser interaction:

POST {{token_endpoint}}
Content-Type: application/x-www-form-urlencoded

grant_type=password
&client_id={{client_id}}
&username={{test_username}}
&password={{test_password}}

Step 3: Validate the token structure. Use Postman test scripts to verify the token contains the expected claims:

pm.test("Token contains required claims", function () {
    const response = pm.response.json();
    const token = response.access_token;
    const payload = JSON.parse(atob(token.split('.')[1]));

    pm.expect(payload.iss).to.include("/realms/" + pm.collectionVariables.get("realm"));
    pm.expect(payload.azp).to.equal(pm.collectionVariables.get("client_id"));
    pm.expect(payload.exp).to.be.above(Math.floor(Date.now() / 1000));
});

Step 4: Refresh the token. Verify that the refresh token works and returns a new access token:

POST {{token_endpoint}}
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&client_id={{client_id}}
&refresh_token={{refresh_token}}

Step 5: Revoke the token. Confirm that token revocation returns a success response:

POST {{revocation_endpoint}}
Content-Type: application/x-www-form-urlencoded

token={{refresh_token}}
&client_id={{client_id}}
&token_type_hint=refresh_token

Testing the Client Credentials Flow

For service-to-service authentication, test the client credentials grant separately:

POST {{token_endpoint}}
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id={{service_client_id}}
&client_secret={{service_client_secret}}

Validate that the resulting token has the correct scope claim and does not contain user-specific claims like preferred_username.

Running Collections in CI/CD with Newman

Newman is the CLI companion to Postman. Export your collection and environment, then run them in your pipeline:

newman run keycloak-tests.json 
  --env-var "keycloak_url=http://localhost:8080" 
  --env-var "realm=test" 
  --env-var "client_id=test-client" 
  --env-var "test_username=test-user" 
  --env-var "test_password=test-password" 
  --reporters cli,junit 
  --reporter-junit-export results.xml

The JUnit reporter produces XML output that most CI systems can parse for test result dashboards.

If you need to debug token issues during development, the SAML Decoder can help inspect SAML assertions when working with federated identity providers connected to Keycloak.

Cypress for End-to-End Login Testing

Integration tests validate that Keycloak is configured correctly. End-to-end tests validate that users can actually log in. This distinction matters because real login flows involve browser redirects, cookies, iframe-based session checks, and JavaScript that unit tests cannot exercise.

Handling the Keycloak Login Page

The core challenge with testing Keycloak login in Cypress is cross-origin navigation. Your application lives on one domain, and the Keycloak login page lives on another. Cypress handles this with cy.origin():

Cypress.Commands.add('keycloakLogin', (username, password) => {
    cy.visit('/');
    cy.origin(Cypress.env('KEYCLOAK_URL'), { args: { username, password } }, ({ username, password }) => {
        cy.get('#username').type(username);
        cy.get('#password').type(password);
        cy.get('#kc-login').click();
    });
});

Register this as a custom command in cypress/support/commands.js and use it in your tests:

describe('Authentication', () => {
    it('should log in and display user profile', () => {
        cy.keycloakLogin('test-user', 'password');

        cy.url().should('not.include', '/auth/');
        cy.get('[data-testid="user-menu"]').should('contain', 'test-user');
    });
});

Testing SSO Across Multiple Applications

Single sign-on is one of the primary reasons teams adopt Keycloak, and it is also one of the hardest features to test manually. With Cypress, you can verify that logging into one application establishes a session that carries over to a second application:

it('should maintain SSO session across applications', () => {
    // Log into application A
    cy.visit(Cypress.env('APP_A_URL'));
    cy.keycloakLogin('test-user', 'password');
    cy.url().should('include', Cypress.env('APP_A_URL'));

    // Visit application B — should not prompt for login
    cy.visit(Cypress.env('APP_B_URL'));
    cy.url().should('not.include', '/auth/');
    cy.get('[data-testid="user-menu"]').should('contain', 'test-user');
});

Testing Logout Flows

Logout is where identity testing gets particularly tricky. Keycloak supports multiple logout mechanisms, and each needs its own test:

it('should perform single logout across all applications', () => {
    cy.keycloakLogin('test-user', 'password');
    cy.visit(Cypress.env('APP_A_URL'));

    // Trigger logout
    cy.get('[data-testid="logout-button"]').click();

    // Verify redirected to post-logout page
    cy.url().should('include', '/logged-out');

    // Verify session is cleared in application B
    cy.visit(Cypress.env('APP_B_URL'));
    cy.url().should('include', '/auth/');
});

Testing Error Scenarios

Your tests should also cover failure paths. These are the scenarios that users will encounter and that support teams will need to troubleshoot:

it('should show error for invalid credentials', () => {
    cy.visit('/');
    cy.origin(Cypress.env('KEYCLOAK_URL'), () => {
        cy.get('#username').type('test-user');
        cy.get('#password').type('wrong-password');
        cy.get('#kc-login').click();

        cy.get('#input-error').should('be.visible')
            .and('contain', 'Invalid username or password');
    });
});

it('should handle locked accounts', () => {
    // Attempt login with wrong password multiple times
    for (let i = 0; i < 5; i++) {
        cy.visit('/');
        cy.origin(Cypress.env('KEYCLOAK_URL'), () => {
            cy.get('#username').type('test-user');
            cy.get('#password').type('wrong-password');
            cy.get('#kc-login').click();
        });
    }

    // Next attempt should show account locked message
    cy.visit('/');
    cy.origin(Cypress.env('KEYCLOAK_URL'), () => {
        cy.get('#username').type('test-user');
        cy.get('#password').type('correct-password');
        cy.get('#kc-login').click();

        cy.get('#input-error').should('contain', 'Account is locked');
    });
});

Testing Custom Themes

If you have customized Keycloak’s login theme, registration pages, or email templates, those customizations need testing too. A theme change that removes the password field or breaks the login form layout will block every user in your system.

Visual Regression Testing with Cypress

Cypress can capture screenshots of each Keycloak page and compare them against baseline images:

describe('Login Theme', () => {
    it('should render the login page correctly', () => {
        cy.visit('/');
        cy.origin(Cypress.env('KEYCLOAK_URL'), () => {
            cy.get('#kc-login').should('be.visible');
            cy.screenshot('login-page', { capture: 'viewport' });
        });
    });

    it('should render the registration page correctly', () => {
        cy.visit('/');
        cy.origin(Cypress.env('KEYCLOAK_URL'), () => {
            cy.get('a[href*="registration"]').click();
            cy.get('#kc-register-form').should('be.visible');
            cy.screenshot('registration-page', { capture: 'viewport' });
        });
    });
});

Pair this with a visual regression plugin like cypress-image-snapshot to automatically detect unintended visual changes.

Testing Themes Across Realms

If you use different themes for different realms (for example, a branded login page for each tenant), parameterize your tests:

const realms = [
    { name: 'tenant-a', expectedLogo: 'tenant-a-logo.png' },
    { name: 'tenant-b', expectedLogo: 'tenant-b-logo.png' },
];

realms.forEach(({ name, expectedLogo }) => {
    it(`should display correct branding for ${name}`, () => {
        cy.visit(`${Cypress.env('KEYCLOAK_URL')}/realms/${name}/account`);
        cy.get('.login-logo img')
            .should('have.attr', 'src')
            .and('include', expectedLogo);
    });
});

For teams using Skycloak’s branding features, automated theme testing ensures that customizations remain consistent across deployments.

Testing Strategy Recommendations

Not all tests are equal in value or cost. Apply the test pyramid to identity infrastructure:

Unit Tests

Test your application’s token validation logic, role-mapping functions, and session management code without any Keycloak dependency. These tests run in milliseconds and catch logic errors early:

  • JWT signature verification with known keys
  • Role-to-permission mapping
  • Token expiration handling
  • Claim extraction and transformation

Integration Tests (Testcontainers)

Test Keycloak configuration and API interactions against a real instance. These tests take seconds to minutes and catch configuration errors:

  • Client configuration (correct grant types, redirect URIs)
  • Role and group assignments propagating to tokens
  • Custom protocol mappers producing expected claims
  • Admin API operations (user CRUD, realm configuration)

End-to-End Tests (Cypress)

Test the complete user experience through a browser. These tests take minutes and catch integration errors between your application and Keycloak:

  • Login and logout flows
  • SSO across applications
  • MFA enrollment and verification
  • Social login redirects
  • Error handling and user-facing messages

Load Tests

Use JMeter or k6 to verify that your Keycloak instance handles expected traffic volumes. Focus on the token endpoint since it receives the most requests in production:

// k6 example
import http from 'k6/http';

export const options = {
    stages: [
        { duration: '1m', target: 100 },
        { duration: '3m', target: 100 },
        { duration: '1m', target: 0 },
    ],
};

export default function () {
    http.post(`${__ENV.KEYCLOAK_URL}/realms/test/protocol/openid-connect/token`, {
        grant_type: 'client_credentials',
        client_id: 'load-test-client',
        client_secret: 'secret',
    });
}

If you are running Keycloak in production and want to avoid managing infrastructure while maintaining performance, Skycloak’s managed hosting handles scaling and monitoring so your team can focus on application development.

CI/CD Pipeline Example

Here is a complete GitHub Actions workflow that runs all three testing layers against a Keycloak instance:

name: Identity Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  integration-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Run Testcontainers tests
        run: ./mvnw verify -Pintegration-tests

  api-tests:
    runs-on: ubuntu-latest
    services:
      keycloak:
        image: quay.io/keycloak/keycloak:26.0
        env:
          KC_BOOTSTRAP_ADMIN_USERNAME: admin
          KC_BOOTSTRAP_ADMIN_PASSWORD: admin
          KC_HEALTH_ENABLED: "true"
        ports:
          - 8080:8080
        options: >-
          --health-cmd "curl -f http://localhost:8080/health/ready || exit 1"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 20
    steps:
      - uses: actions/checkout@v4
      - name: Import test realm
        run: |
          docker exec ${{ job.services.keycloak.id }} 
            /opt/keycloak/bin/kcadm.sh config credentials 
            --server http://localhost:8080 
            --realm master --user admin --password admin
          docker cp test-realm.json ${{ job.services.keycloak.id }}:/tmp/
          docker exec ${{ job.services.keycloak.id }} 
            /opt/keycloak/bin/kcadm.sh create realms 
            -f /tmp/test-realm.json
      - name: Run Newman tests
        run: |
          npm install -g newman
          newman run tests/keycloak-tests.json 
            --env-var "keycloak_url=http://localhost:8080" 
            --env-var "realm=test" 
            --reporters cli,junit 
            --reporter-junit-export results/api-tests.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: api-test-results
          path: results/

  e2e-tests:
    runs-on: ubuntu-latest
    services:
      keycloak:
        image: quay.io/keycloak/keycloak:26.0
        env:
          KC_BOOTSTRAP_ADMIN_USERNAME: admin
          KC_BOOTSTRAP_ADMIN_PASSWORD: admin
        ports:
          - 8080:8080
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Install dependencies
        run: npm ci
      - name: Start application
        run: npm start &
      - name: Run Cypress tests
        uses: cypress-io/github-action@v6
        env:
          CYPRESS_KEYCLOAK_URL: http://localhost:8080
        with:
          wait-on: http://localhost:3000
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: cypress-screenshots
          path: cypress/screenshots/

This pipeline runs integration tests, API tests, and end-to-end tests in parallel. Each job gets its own Keycloak instance, so there is no shared state between test suites.

For teams that want to generate the initial Keycloak Docker setup quickly, the Docker Compose Generator can scaffold a working docker-compose.yml with database, Keycloak, and health checks pre-configured.

Conclusion

Testing identity infrastructure requires a layered approach. Testcontainers gives you isolated, reproducible integration tests that verify Keycloak configuration at the API level. Postman and Newman let you validate OIDC protocol flows and run them as part of your CI pipeline. Cypress tests the actual user experience, catching cross-origin issues, theme regressions, and logout edge cases that API tests miss.

The investment pays off quickly. A misconfigured client scope or a broken login theme costs hours of debugging and user frustration in production. Catching those issues in CI costs minutes.

Start with the layer closest to where your team has experienced the most identity-related incidents. If token claims are frequently wrong, start with Testcontainers. If login flows break after deployments, start with Cypress. Then expand outward until you have coverage across all three layers.

For comprehensive reference on configuring the features these tests exercise, check the Skycloak documentation. And if you want to focus your testing effort on your applications rather than on the Keycloak infrastructure itself, Skycloak’s managed platform handles the operational complexity so you can focus on building great products.

Ready to simplify your Keycloak infrastructure? Try Skycloak free.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman