Testing Keycloak: Automated Validation with Testcontainers, Postman, and Cypress
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.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.