This sample shows how to contract test a Spring Boot API from a single OpenAPI spec when different endpoints use different authentication schemes and OAuth2 RBAC:
- OAuth2 bearer tokens for role-protected
POSTendpoints- Users with the
usersrole can place orders viaPOST /ordersandPOST /orders/{id}. - Users with the
adminsrole can create products viaPOST /productsandPOST /products/{id}. - Role-mismatch cases result in forbidden (
403) scenarios.
- Users with the
GETendpoints: Basic Auth for endpoints secured with HTTP Basic authentication.DELETEendpoints: API key authentication using theX-API-Keyheader for delete endpoints secured with API keys.
Spec used in this project:
Keycloakacts as the OAuth2 authorization server for real OAuth2 runs.Order API(Spring Boot) is the system under test and enforces:- OAuth2 bearer-token authentication for OAuth-protected endpoints
- RBAC on OAuth-protected endpoints using the
usersandadminsroles - Basic Auth for endpoints secured with HTTP Basic authentication
- API key authentication for endpoints secured with
X-API-Key
Specmaticreads the OpenAPI spec and sends requests with the appropriate auth headers during contract tests.- OAuth/RBAC contract examples use Specmatic fixtures:
- A
beforefixture fetches an OAuth token from Keycloak before the protected API request runs. - The fixture captures
access_tokenasACCESS_TOKEN. - The contract example then uses the captured value as
Authorization: Bearer $(ACCESS_TOKEN).
- A
- Fast local contract tests keep this flow mocked internally.
- Testcontainers-based contract tests run the same fixture-driven flow against real Keycloak.
Relevant code:
- Prod security config:
src/main/java/com/store/config/SecurityConfig.kt - Test-mode dummy security config:
src/test/java/com/store/config/DummySecurityConfig.kt - Dummy auth header validation filter:
src/test/java/com/store/security/DummySecurityFilter.kt - Specmatic config (security tokens + base URL):
specmatic.yaml
For fast local contract tests, the app runs in test mode and mocks OAuth token retrieval internally instead of talking to Keycloak. This keeps the local JUnit contract test focused on API behavior and contract conformance.
The OAuth/RBAC examples still model the real flow using Specmatic fixtures.
The before fixture performs the token request and captures ACCESS_TOKEN, and the protected API request uses that captured token.
In the fast local JUnit test, the OAuth fetch is mocked internally. In the Testcontainers test, the same fixture flow calls real Keycloak.
Use the Testcontainers mode when you want to verify the application against a real OAuth issuer, real JWTs, and role-specific access rules.
This starts the Spring app in-process using the test profile and runs Specmatic contract tests against it. OAuth token fetching is mocked internally for this mode.
- Test class:
src/test/java/com/store/ContractTest.java - Command:
./gradlew test --tests com.store.ContractTestWhen to use:
- Fast feedback while developing endpoints/spec
- No Keycloak required
- No Docker required
This mode runs:
- Spring Boot app in
prodprofile - Keycloak in a Testcontainer
- Specmatic in a Testcontainer
The OAuth/RBAC contract examples use before fixtures to fetch real role-specific OAuth tokens from Keycloak before the protected API requests run.
This is the main verification path for the real OAuth flow and RBAC behavior.
- Test class:
src/test/java/com/store/ContractTestUsingTestContainerTest.java - Command:
./gradlew test --tests com.store.ContractTestUsingTestContainerTestWhen to use:
- Validate the app with real Keycloak + JWT issuer configuration
- Verify role-specific access for
usersandadminsOAuth tokens - Reproduce an environment closer to deployment while still running from JUnit
Prerequisite:
- Docker running locally
This mode runs all components in containers:
- Keycloak
- Order API
- Specmatic test runner
Run:
./gradlew clean assembledocker compose -f docker-compose-test.yaml up --build specmatic-testExpected result:
- Compose exits with code
0 - Specmatic output includes
Failures: 0
Reports generated:
build/reports/specmatic/test/html/index.htmlbuild/reports/specmatic/test/ctrf/ctrf-report.json
Cleanup:
docker compose -f docker-compose-test.yaml downOAuth/RBAC behavior is represented through Specmatic fixtures in the contract example JSON files.
Each OAuth example has this structure:
-
A
beforefixture calls the Keycloak token endpoint:POST /realms/specmatic/protocol/openid-connect/token -
The fixture sends the password-grant form body using either the user credentials or the admin credentials:
grant_type=password&client_id=order-api&username=...&password=... -
The fixture expects a
200response from Keycloak and captures the token:{ "access_token": "(ACCESS_TOKEN:string)" } -
The protected API request then uses the captured token:
{ "Authorization": "Bearer $(ACCESS_TOKEN)" }
This means the examples verify both sides of the RBAC rule:
userstokens are accepted for order operations and rejected for product operations.adminstokens are accepted for product operations and rejected for order operations.
The most useful thing in the logs is whether Specmatic is using the correct auth header for each secured operation, and whether OAuth-protected endpoints receive a token with the expected role.
POST /orders->Authorization: Bearer ...with theusersrolePOST /orders/{id}->Authorization: Bearer ...with theusersrolePOST /products->Authorization: Bearer ...with theadminsrolePOST /products/{id}->Authorization: Bearer ...with theadminsrole
For endpoints secured with Basic Auth or API key authentication, Specmatic should send the corresponding header configured for that security scheme. Examples:
Request to http://localhost:8080
GET /products/10
Authorization: Basic dXNlcjpwYXNzd29yZA==
Request to http://localhost:8080
DELETE /products/20
X-API-Key: APIKEY1234
- Specmatic finishes with
Failures: 0. - OAuth-protected endpoints do not repeatedly fail with unexpected
401 Unauthorizedor403 Forbiddenresponses. - The expected forbidden examples return
403 Forbidden. - Requests use the authentication scheme expected by the OpenAPI operation.
- In Testcontainers mode,
beforefixtures successfully fetch real tokens from Keycloak. userstokens are accepted for order endpoints and rejected for product endpoints.adminstokens are accepted for product endpoints and rejected for order endpoints.
401 Unauthorizedon an OAuth-protected endpoint:- Missing/invalid bearer header
- In real OAuth modes, token fetch / Keycloak / issuer config problem
- Unexpected
403 Forbiddenon an OAuth-protected endpoint:- Token is valid, but does not have the role required by the endpoint
usertoken used for an admin-only product endpointadmintoken used for a user-only order endpoint- Role mapping not configured as expected in Keycloak
401 Unauthorizedon a Basic Auth endpoint:- Missing or malformed Basic Auth header
401 Unauthorizedon an API key endpoint:- Missing
X-API-Key
- Missing
- Connection errors to
localhost:8080/order-api:8080/keycloak:8080:- App or Keycloak not started yet
- Wrong
APP_BASE_URLorKEYCLOAK_BASE_URL
OAuth/RBAC examples fetch tokens through before fixtures:
- The fixture calls Keycloak's token endpoint.
- The fixture captures
access_tokenasACCESS_TOKEN. - The protected API request uses
Authorization: Bearer $(ACCESS_TOKEN).
In the local JUnit contract test, that OAuth fetch is mocked internally.
In the Testcontainers contract test, the fixture fetches a real role-specific OAuth token from Keycloak before the protected API request runs.
Other configured auth values come from specmatic.yaml:
- Basic Auth token value, for example
BASIC_AUTH_TOKENwith defaultdXNlcjpwYXNzd29yZA== - API key value, for example
API_KEYwith defaultAPIKEY1234
This is for manually trying the app with real auth behavior using the prod profile.
From the project root:
docker compose upThis starts Keycloak on http://localhost:8083 and imports the specmatic realm.
Unix / macOS / PowerShell:
./gradlew clean bootRun --args='--spring.profiles.active=prod'Windows CMD:
gradlew.bat clean bootRun --args="--spring.profiles.active=prod"App runs on http://localhost:8080.
Fetch a token for a user with the users role:
USER_TOKEN=$(curl -fsS -X POST http://localhost:8083/realms/specmatic/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=password" \
--data-urlencode "client_id=order-api" \
--data-urlencode "username=user1" \
--data-urlencode "password=password" \
--data-urlencode "scope=profile email" | jq -r '.access_token')Fetch a token for a user with the admins role:
ADMIN_TOKEN=$(curl -fsS -X POST http://localhost:8083/realms/specmatic/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=password" \
--data-urlencode "client_id=order-api" \
--data-urlencode "username=admin1" \
--data-urlencode "password=password" \
--data-urlencode "scope=profile email" | jq -r '.access_token')If jq is not installed, copy the access_token manually from the JSON response.
If your imported realm uses different sample usernames or passwords, update the username and password values in the commands above.
curl -i -X POST http://localhost:8080/orders \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"count": 1,
"productid": 10,
"status": "pending"
}'Expected: success response for the order operation with an id field in the response body.
curl -i -X POST http://localhost:8080/orders/10 \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"count": 1,
"productid": 10,
"status": "fulfilled"
}'Expected: success response with status code 200.
curl -i -X POST http://localhost:8080/products \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "other",
"inventory": 100,
"name": "Test Product"
}'Expected: success response for the product operation, with id field in the response body.
curl -i -X POST http://localhost:8080/products/20 \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "other",
"inventory": 100,
"name": "Test Product"
}'Expected: success response, with status code 200.
A valid token with the wrong role should return 403 Forbidden.
Admin token against an order endpoint:
curl -i -X POST http://localhost:8080/orders \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"count": 1,
"productid": 10,
"status": "fulfilled"
}'Expected: 403 Forbidden.
User token against a product endpoint:
curl -i -X POST http://localhost:8080/products \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "other",
"inventory": 100,
"name": "Test Product"
}'Expected: 403 Forbidden.
curl -i -u user:password http://localhost:8080/products/10Expected: success response from the Basic Auth secured endpoint.
curl -i -X DELETE http://localhost:8080/products/20 \
-H "X-API-Key: APIKEY1234"Expected: success response, with status code 200.
This project uses one OpenAPI spec with multiple security schemes in spec/order-api-with-auth.yaml:
oAuth2AuthCode(OAuth2)basicAuth(HTTP Basic)apiKeyAuth(header API key)
OAuth2-protected operations also use role-based access control in the application:
userrole for order operationsadminrole for product operations
Specmatic maps security schemes to configured auth values and fixture-generated values. For OAuth/RBAC examples, the value comes from the before fixture capture. For the mocked local contract test, the OAuth fetch is mocked internally. For the Testcontainers contract test, the fixture fetches real role-specific OAuth tokens from Keycloak.
You can inspect the imported realm in the Keycloak admin console:
- URL:
http://localhost:8083 - Admin username:
admin - Admin password:
admin
