diff --git a/api-security-schemes/README.md b/api-security-schemes/README.md index 90950b9..3b64ef4 100644 --- a/api-security-schemes/README.md +++ b/api-security-schemes/README.md @@ -3,7 +3,7 @@ This project demonstrates how to leverage OpenAPI specifications as a Contract Test with Specmatic when the specification includes multiple [security schemes](https://spec.openapis.org/oas/v3.0.1#security-scheme-object) to protect different endpoints based on HTTP methods. -Run the contract tests, observe the authentication failures, then fix [`specmatic.yaml`](specmatic.yaml) so Specmatic sends the right OAuth2, Basic Auth, and API key values. +The lab starts in an intentionally broken state. Your job is to observe the failures, understand how each auth mechanism is wired, and make the smallest fixes needed to get the contract tests passing again. ## Time required to complete this lab 10-15 minutes. @@ -16,10 +16,9 @@ Run the contract tests, observe the authentication failures, then fix [`specmati - `Keycloak` acts as the OAuth2 authorization server for `POST` requests. - `Order API` is the system under test and enforces: - - OAuth2 for `POST` + - OAuth2 + RBAC for `POST` - Basic Auth for `GET` - - API key for `DELETE` -- `Specmatic` reads the OpenAPI contract, looks up matching `securitySchemes` entries in `specmatic.yaml`, and sends auth headers during contract tests. + - API key auth for `DELETE` The application validates OAuth2 tokens by calling the ```spring.security.oauth2.resourceserver.jwt.issuer-uri``` url defined in the ```application.properties``` file. @@ -27,7 +26,7 @@ The application validates OAuth2 tokens by calling the ```spring.security.oauth2 The application uses three security schemes: -- **OAuth2 (POST endpoints)**: Requires a bearer token with the `email` scope. +- **OAuth2 (POST endpoints)**: Requires a bearer token with the appropriate role. - **Basic Authentication (GET endpoints)**: Requires valid username/password credentials. - **API Key (DELETE endpoints)**: Requires a valid `X-API-Key` header. @@ -47,7 +46,7 @@ The OpenAPI contract defines these schemes: email: email ``` -**2. GET endpoints with Basic Auth*: +**2. GET endpoints with Basic Auth**: ```yaml securitySchemes: basicAuth: @@ -66,6 +65,8 @@ The OpenAPI contract defines these schemes: description: API Key based authentication ``` +## Basic and API Key + Specmatic will check if these security schemes are defined in the ```specmatic.yaml``` configuration. If found, it will use the configured values; otherwise, it will auto-generate appropriate authentication headers. @@ -75,9 +76,6 @@ specs: - spec: id: orderApiSpec securitySchemes: - oAuth2AuthCode: - type: oauth2 - token: ${INVALID_OAUTH_TOKEN:OAUTH1234} basicAuth: type: basicAuth token: ${BASIC_AUTH_TOKEN:dXNlcjppbnZhbGlkcGFzcw==} @@ -86,15 +84,32 @@ specs: token: ${API_KEY:INVALID_APIKEY1234} ``` +## OAuth with RBAC + +Unlike the Basic Auth and API key schemes, OAuth is not configured here as a single static token value in [`specmatic.yaml`](specmatic.yaml). + +Instead, the OAuth/RBAC `POST` examples under [`auth_examples/`](./auth_examples) use Specmatic `before` fixtures: +1. The `before` fixture calls the Keycloak token endpoint. +2. The fixture captures `access_token` as `ACCESS_TOKEN`. +3. The protected API request should use this captured token in the `Authorization` header. + +This sample defines two roles: `users` and `admins`, Users can interact with the order `POST` endpoints, while admins can interact with the product `POST` endpoints. + ## Lab Rules -- Do not edit the specs checked out under `.specmatic/repos/labs-contracts/openapi/security/` in this lab. - Do not edit `docker-compose.yaml`. -- Edit only [`specmatic.yaml`](specmatic.yaml). +- Do not edit the specs checked out under `.specmatic/repos/labs-contracts/openapi/security/` in this lab. +- Edit only [`specmatic.yaml`](specmatic.yaml) and the example files under [`auth_examples/`](./auth_examples). ## Intentional Failure -Because the OUATH token name and the default values for the BASIC_AUTH_TOKEN and API_KEY in `specmatic.yaml` are intentionally wrong, the protected endpoints return `401 Unauthorized`. +This lab is intentionally broken in two places: +1. The static auth values in [`specmatic.yaml`](specmatic.yaml) are wrong for `Basic` and `apiKey` Schemes. +2. The [OAuth example](./auth_examples) files are missing the `Authorization` header in the `POST` requests + +Because of that: +- Basic Auth and API key requests fail with invalid credentials +- OAuth `before` fixtures still run, but the protected `POST` requests do not send the fetched token ## Run the failing tests @@ -107,14 +122,21 @@ docker compose up specmatic-test --abort-on-container-exit Expected failing result: ```terminaloutput -Tests run: 171, Successes: 0, Failures: 171, WIP: 0, Errors: 0 +Tests run: 185, Successes: 152, Failures: 33, WIP: 0, Errors: 0 ``` - The compose command exits with a non-zero code. - Specmatic reports failures against secured endpoints. - The failures include `401 Unauthorized` responses. -Since OUATH token name and the default values for the BASIC_AUTH_TOKEN and API_KEY in [`specmatic.yaml`](specmatic.yaml) is invalid, Specmatic sends invalid credentials and the protected endpoints return `401 Unauthorized`. +For Basic and API Key, the failures are due to: +- Invalid `APIKey` credentials +- Invalid `Basic` Auth credentials (should be valid base64 encoded credentials) + +For the OAuth failures, look carefully at the logs: +- The `before` fixture should run +- The token fetch request to Keycloak should succeed +- However, the protected `POST` request will still fail because it is not using the captured token Cleanup after each run: @@ -124,18 +146,24 @@ docker compose down -v ## Your Task -Update [`specmatic.yaml`](specmatic.yaml) to valid values again: +### 1. Fix the static auth config in `specmatic.yaml` -- `INVALID_OAUTH_TOKEN` to `OAUTH_TOKEN` -- `dXNlcjppbnZhbGlkcGFzcw==` to `dXNlcjpwYXNzd29yZA==` -- `INVALID_APIKEY1234` to `APIKEY1234` +Update the fallback values in [`specmatic.yaml`](specmatic.yaml): +- change `dXNlcjppbnZhbGlkcGFzcw==` to `dXNlcjpwYXNzd29yZA==` +- change `INVALID_APIKEY1234` to `APIKEY1234` -Do not change anything else. Fix only the above values. +### 2. Restore the bearer token wiring in the OAuth examples -Alternatively, just run the following command: +Inspect all `POST` examples under [`auth_examples/`](./auth_examples), including both success and forbidden variants. +In each request, add the `Authorization` header using the captured `ACCESS_TOKEN` from the `before` fixture: +```plaintext +"Authorization": "Bearer $(ACCESS_TOKEN)" +``` + +Alternatively, just run the following command: ```shell -docker run --rm --entrypoint sh -v "${PWD}:/usr/src/app" specmatic/enterprise -lc "sed -i 's#INVALID_OAUTH_TOKEN#OAUTH_TOKEN#g; s#dXNlcjppbnZhbGlkcGFzcw==#dXNlcjpwYXNzd29yZA==#g; s#INVALID_APIKEY1234#APIKEY1234#g' specmatic.yaml" +docker run --rm --entrypoint bash -v "${PWD}:/usr/src/app" -w /usr/src/app specmatic/enterprise -lc 'sed -i "s#dXNlcjppbnZhbGlkcGFzcw==#dXNlcjpwYXNzd29yZA==#g; s#INVALID_APIKEY1234#APIKEY1234#g" specmatic.yaml; find auth_examples -type f -name "*.json" -print0 | while IFS= read -r -d "" file; do tmp="$(mktemp)"; jq --arg auth "Bearer \$(ACCESS_TOKEN)" "if ((.partial?[\"http-request\"]?.method? // \"\" | ascii_upcase) == \"POST\") then .partial[\"http-request\"].headers.Authorization = \$auth else . end" "$file" > "$tmp" && mv "$tmp" "$file"; done' ``` ## Verify the fix @@ -149,7 +177,7 @@ docker compose up specmatic-test --abort-on-container-exit Expected result: ```terminaloutput -Tests run: 171, Successes: 171, Failures: 0, WIP: 0, Errors: 0 +Tests run: 185, Successes: 185, Failures: 0, WIP: 0, Errors: 0 ``` Cleanup after run: @@ -166,7 +194,8 @@ Generated test reports: - If Docker ports `8080` or `8083` are already in use, stop the conflicting process and try again. - If containers from a previous run are still present, run `docker compose down -v` before retrying. - If Keycloak takes a little longer to start, wait for the compose run to finish; the test container already includes readiness checks. -- If tests still fail after your change, compare the three token fallback values in `specmatic.yaml` against the valid values shown in the lab. +- If `GET` or `DELETE` requests still fail, re-check the Basic Auth and API key fallback values in [`specmatic.yaml`](specmatic.yaml). +- If the OAuth token request succeeds but the protected `POST` still fails, check whether the example request is missing the `Authorization` header. ## Next step If you are doing this lab as part of an eLearning course, return to the eLearning site and continue with the next module. diff --git a/api-security-schemes/auth_examples/delete-orders-id_401.json b/api-security-schemes/auth_examples/delete-orders-id_401.json new file mode 100644 index 0000000..bb94ad1 --- /dev/null +++ b/api-security-schemes/auth_examples/delete-orders-id_401.json @@ -0,0 +1,24 @@ +{ + "http-request": { + "path": "/orders/292", + "method": "DELETE", + "headers": { + "X-API-Key": "kv-9f75a4e638480b91" + }, + "body": "" + }, + "http-response": { + "status": 401, + "body": { + "error": "Not Acceptable", + "timestamp": "2026-07-01T12:26:23+05:30", + "status": 401, + "message": "Unauthorized", + "path": "/orders/292" + }, + "status-text": "Unauthorized", + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/api-security-schemes/auth_examples/delete-products-id_401.json b/api-security-schemes/auth_examples/delete-products-id_401.json new file mode 100644 index 0000000..27010bf --- /dev/null +++ b/api-security-schemes/auth_examples/delete-products-id_401.json @@ -0,0 +1,24 @@ +{ + "http-request": { + "path": "/products/567.0", + "method": "DELETE", + "headers": { + "X-API-Key": "kv-e701d678df1604bf" + }, + "body": "" + }, + "http-response": { + "status": 401, + "body": { + "error": "Loop Detected", + "timestamp": "2026-07-01T12:26:22+05:30", + "status": 401, + "message": "Unauthorized", + "path": "/products/567.0" + }, + "status-text": "Unauthorized", + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/api-security-schemes/auth_examples/get-orders-id_401.json b/api-security-schemes/auth_examples/get-orders-id_401.json new file mode 100644 index 0000000..eeff395 --- /dev/null +++ b/api-security-schemes/auth_examples/get-orders-id_401.json @@ -0,0 +1,24 @@ +{ + "http-request": { + "path": "/orders/105", + "method": "GET", + "headers": { + "Authorization": "Basic RVlXRlg6RU1GQ0Q=" + }, + "body": "" + }, + "http-response": { + "status": 401, + "body": { + "error": "Not Found", + "timestamp": "2026-07-01T12:26:23+05:30", + "status": 401, + "message": "Unauthorized", + "path": "/orders/105" + }, + "status-text": "Unauthorized", + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/api-security-schemes/auth_examples/get-orders_401.json b/api-security-schemes/auth_examples/get-orders_401.json new file mode 100644 index 0000000..228e6ae --- /dev/null +++ b/api-security-schemes/auth_examples/get-orders_401.json @@ -0,0 +1,28 @@ +{ + "http-request": { + "path": "/orders", + "method": "GET", + "query": { + "productid": "294", + "status": "Never_married" + }, + "headers": { + "Authorization": "Basic UUtHTlY6VFJRS0k=" + }, + "body": "" + }, + "http-response": { + "status": 401, + "body": { + "error": "Conflict", + "timestamp": "2026-07-01T12:26:22+05:30", + "status": 401, + "message": "Unauthorized", + "path": "/orders" + }, + "status-text": "Unauthorized", + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/api-security-schemes/auth_examples/get-product-id_401.json b/api-security-schemes/auth_examples/get-product-id_401.json new file mode 100644 index 0000000..9144052 --- /dev/null +++ b/api-security-schemes/auth_examples/get-product-id_401.json @@ -0,0 +1,24 @@ +{ + "http-request": { + "path": "/products/680.0", + "method": "GET", + "headers": { + "Authorization": "Basic R1NUSEE6QlRPTVE=" + }, + "body": "" + }, + "http-response": { + "status": 401, + "body": { + "error": "Gone", + "timestamp": "2026-07-01T12:26:22+05:30", + "status": 401, + "message": "Unauthorized", + "path": "/products/680.0" + }, + "status-text": "Unauthorized", + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/api-security-schemes/auth_examples/get-products_401.json b/api-security-schemes/auth_examples/get-products_401.json new file mode 100644 index 0000000..c05800f --- /dev/null +++ b/api-security-schemes/auth_examples/get-products_401.json @@ -0,0 +1,27 @@ +{ + "http-request": { + "path": "/products", + "method": "GET", + "query": { + "type": "gadget" + }, + "headers": { + "Authorization": "Basic UUJIUVM6RFFFWEs=" + }, + "body": "" + }, + "http-response": { + "status": 401, + "body": { + "error": "Interval Too Brief", + "timestamp": "2026-07-01T12:26:22+05:30", + "status": 401, + "message": "Unauthorized", + "path": "/products" + }, + "status-text": "Unauthorized", + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/api-security-schemes/auth_examples/post-orders-create-forbidden.json b/api-security-schemes/auth_examples/post-orders-create-forbidden.json new file mode 100644 index 0000000..116359c --- /dev/null +++ b/api-security-schemes/auth_examples/post-orders-create-forbidden.json @@ -0,0 +1,50 @@ +{ + "before": [ + { + "type": "http", + "http-request": { + "baseUrl": "${KEYCLOAK_BASE_URL:http://keycloak:8080}", + "path": "/realms/specmatic/protocol/openid-connect/token", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=password&client_id=order-api&username=${KEYCLOAK_ADMIN_USERNAME:admin1}&password=${KEYCLOAK_ADMIN_PASSWORD:password}" + }, + "http-response": { + "status": 200, + "body": { + "access_token": "(ACCESS_TOKEN:string)" + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ], + "partial": { + "http-request": { + "path": "/orders", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "productid": 10 + } + }, + "http-response": { + "status": 403, + "body": { + "timestamp": "2026-07-01T12:26:22+05:30", + "status": 403, + "error": "Forbidden", + "message": "(string)" + }, + "status-text": "Forbidden", + "headers": { + "Content-Type": "application/json" + } + } + } +} diff --git a/api-security-schemes/auth_examples/post-orders-create.json b/api-security-schemes/auth_examples/post-orders-create.json new file mode 100644 index 0000000..4f2b2fb --- /dev/null +++ b/api-security-schemes/auth_examples/post-orders-create.json @@ -0,0 +1,47 @@ +{ + "before": [ + { + "type": "http", + "executeFor": { + "scenarios": "all" + }, + "http-request": { + "baseUrl": "${KEYCLOAK_BASE_URL:http://keycloak:8080}", + "path": "/realms/specmatic/protocol/openid-connect/token", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=password&client_id=order-api&username=${KEYCLOAK_USER_USERNAME:user1}&password=${KEYCLOAK_USER_PASSWORD:password}" + }, + "http-response": { + "status": 200, + "body": { + "access_token": "(ACCESS_TOKEN:string)" + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ], + "partial": { + "http-request": { + "path": "/orders", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "productid": 10 + } + }, + "http-response": { + "status": 200, + "body": {}, + "headers": { + "Content-Type": "application/json" + } + } + } +} diff --git a/api-security-schemes/auth_examples/post-orders-id_application_json_401.json b/api-security-schemes/auth_examples/post-orders-id_application_json_401.json new file mode 100644 index 0000000..8bea5b1 --- /dev/null +++ b/api-security-schemes/auth_examples/post-orders-id_application_json_401.json @@ -0,0 +1,30 @@ +{ + "http-request": { + "path": "/orders/460", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer nisi" + }, + "body": { + "id": 689, + "productid": 121, + "count": 778, + "status": "fulfilled" + } + }, + "http-response": { + "status": 401, + "body": { + "error": "Forbidden", + "timestamp": "2026-07-01T12:26:23+05:30", + "status": 401, + "message": "Unauthorized", + "path": "/orders/460" + }, + "status-text": "Unauthorized", + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/api-security-schemes/auth_examples/post-orders-update-forbidden.json b/api-security-schemes/auth_examples/post-orders-update-forbidden.json new file mode 100644 index 0000000..4e85416 --- /dev/null +++ b/api-security-schemes/auth_examples/post-orders-update-forbidden.json @@ -0,0 +1,48 @@ +{ + "before": [ + { + "type": "http", + "http-request": { + "baseUrl": "${KEYCLOAK_BASE_URL:http://keycloak:8080}", + "path": "/realms/specmatic/protocol/openid-connect/token", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=password&client_id=order-api&username=${KEYCLOAK_ADMIN_USERNAME:admin1}&password=${KEYCLOAK_ADMIN_PASSWORD:password}" + }, + "http-response": { + "status": 200, + "body": { + "access_token": "(ACCESS_TOKEN:string)" + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ], + "partial": { + "http-request": { + "path": "/orders/10", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": {} + }, + "http-response": { + "status": 403, + "body": { + "timestamp": "2026-07-01T12:26:23+05:30", + "status": 403, + "error": "Forbidden", + "message": "(string)" + }, + "status-text": "Forbidden", + "headers": { + "Content-Type": "application/json" + } + } + } +} diff --git a/api-security-schemes/auth_examples/post-orders-update.json b/api-security-schemes/auth_examples/post-orders-update.json new file mode 100644 index 0000000..d7bdc7f --- /dev/null +++ b/api-security-schemes/auth_examples/post-orders-update.json @@ -0,0 +1,45 @@ +{ + "before": [ + { + "type": "http", + "executeFor": { + "scenarios": "all" + }, + "http-request": { + "baseUrl": "${KEYCLOAK_BASE_URL:http://keycloak:8080}", + "path": "/realms/specmatic/protocol/openid-connect/token", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=password&client_id=order-api&username=${KEYCLOAK_USER_USERNAME:user1}&password=${KEYCLOAK_USER_PASSWORD:password}" + }, + "http-response": { + "status": 200, + "body": { + "access_token": "(ACCESS_TOKEN:string)" + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ], + "partial": { + "http-request": { + "path": "/orders/(id:number)", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": {} + }, + "http-response": { + "status": 200, + "body": "success", + "headers": { + "Content-Type": "text/plain" + } + } + } +} diff --git a/api-security-schemes/auth_examples/post-orders_application_json_401.json b/api-security-schemes/auth_examples/post-orders_application_json_401.json new file mode 100644 index 0000000..72eb4fd --- /dev/null +++ b/api-security-schemes/auth_examples/post-orders_application_json_401.json @@ -0,0 +1,29 @@ +{ + "http-request": { + "path": "/orders", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer quasi" + }, + "body": { + "productid": 480, + "count": 688, + "status": "fulfilled" + } + }, + "http-response": { + "status": 401, + "body": { + "error": "Unsupported URI Scheme", + "timestamp": "2026-07-01T12:26:22+05:30", + "status": 401, + "message": "Unauthorized", + "path": "/orders" + }, + "status-text": "Unauthorized", + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/api-security-schemes/auth_examples/post-products-create-forbidden.json b/api-security-schemes/auth_examples/post-products-create-forbidden.json new file mode 100644 index 0000000..c7a2927 --- /dev/null +++ b/api-security-schemes/auth_examples/post-products-create-forbidden.json @@ -0,0 +1,48 @@ +{ + "before": [ + { + "type": "http", + "http-request": { + "baseUrl": "${KEYCLOAK_BASE_URL:http://keycloak:8080}", + "path": "/realms/specmatic/protocol/openid-connect/token", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=password&client_id=order-api&username=${KEYCLOAK_USER_USERNAME:user1}&password=${KEYCLOAK_USER_PASSWORD:password}" + }, + "http-response": { + "status": 200, + "body": { + "access_token": "(ACCESS_TOKEN:string)" + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ], + "partial": { + "http-request": { + "path": "/products", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": {} + }, + "http-response": { + "status": 403, + "body": { + "timestamp": "2026-07-01T12:26:22+05:30", + "status": 403, + "error": "Forbidden", + "message": "(string)" + }, + "status-text": "Forbidden", + "headers": { + "Content-Type": "application/json" + } + } + } +} diff --git a/api-security-schemes/auth_examples/post-products-create.json b/api-security-schemes/auth_examples/post-products-create.json new file mode 100644 index 0000000..189c866 --- /dev/null +++ b/api-security-schemes/auth_examples/post-products-create.json @@ -0,0 +1,45 @@ +{ + "before": [ + { + "type": "http", + "executeFor": { + "scenarios": "all" + }, + "http-request": { + "baseUrl": "${KEYCLOAK_BASE_URL:http://keycloak:8080}", + "path": "/realms/specmatic/protocol/openid-connect/token", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=password&client_id=order-api&username=${KEYCLOAK_ADMIN_USERNAME:admin1}&password=${KEYCLOAK_ADMIN_PASSWORD:password}" + }, + "http-response": { + "status": 200, + "body": { + "access_token": "(ACCESS_TOKEN:string)" + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ], + "partial": { + "http-request": { + "path": "/products", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": {} + }, + "http-response": { + "status": 200, + "body": {}, + "headers": { + "Content-Type": "application/json" + } + } + } +} diff --git a/api-security-schemes/auth_examples/post-products-id_application_json_401.json b/api-security-schemes/auth_examples/post-products-id_application_json_401.json new file mode 100644 index 0000000..7c9655c --- /dev/null +++ b/api-security-schemes/auth_examples/post-products-id_application_json_401.json @@ -0,0 +1,30 @@ +{ + "http-request": { + "path": "/products/449.0", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer provident" + }, + "body": { + "id": 674, + "name": "Sleek Marble Chair", + "type": "book", + "inventory": 406 + } + }, + "http-response": { + "status": 401, + "body": { + "error": "Extension Required", + "timestamp": "2026-07-01T12:26:22+05:30", + "status": 401, + "message": "Unauthorized", + "path": "/products/449.0" + }, + "status-text": "Unauthorized", + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/api-security-schemes/auth_examples/post-products-update-forbidden.json b/api-security-schemes/auth_examples/post-products-update-forbidden.json new file mode 100644 index 0000000..d8c6601 --- /dev/null +++ b/api-security-schemes/auth_examples/post-products-update-forbidden.json @@ -0,0 +1,48 @@ +{ + "before": [ + { + "type": "http", + "http-request": { + "baseUrl": "${KEYCLOAK_BASE_URL:http://keycloak:8080}", + "path": "/realms/specmatic/protocol/openid-connect/token", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=password&client_id=order-api&username=${KEYCLOAK_USER_USERNAME:user1}&password=${KEYCLOAK_USER_PASSWORD:password}" + }, + "http-response": { + "status": 200, + "body": { + "access_token": "(ACCESS_TOKEN:string)" + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ], + "partial": { + "http-request": { + "path": "/products/10", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": {} + }, + "http-response": { + "status": 403, + "body": { + "timestamp": "2026-07-01T12:26:22+05:30", + "status": 403, + "error": "Forbidden", + "message": "(string)" + }, + "status-text": "Forbidden", + "headers": { + "Content-Type": "application/json" + } + } + } +} diff --git a/api-security-schemes/auth_examples/post-products-update.json b/api-security-schemes/auth_examples/post-products-update.json new file mode 100644 index 0000000..14f440d --- /dev/null +++ b/api-security-schemes/auth_examples/post-products-update.json @@ -0,0 +1,45 @@ +{ + "before": [ + { + "type": "http", + "executeFor": { + "scenarios": "all" + }, + "http-request": { + "baseUrl": "${KEYCLOAK_BASE_URL:http://keycloak:8080}", + "path": "/realms/specmatic/protocol/openid-connect/token", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "grant_type=password&client_id=order-api&username=${KEYCLOAK_ADMIN_USERNAME:admin1}&password=${KEYCLOAK_ADMIN_PASSWORD:password}" + }, + "http-response": { + "status": 200, + "body": { + "access_token": "(ACCESS_TOKEN:string)" + }, + "headers": { + "Content-Type": "application/json" + } + } + } + ], + "partial": { + "http-request": { + "path": "/products/10", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": {} + }, + "http-response": { + "status": 200, + "body": "success", + "headers": { + "Content-Type": "text/plain" + } + } + } +} diff --git a/api-security-schemes/auth_examples/post-products_application_json_401.json b/api-security-schemes/auth_examples/post-products_application_json_401.json new file mode 100644 index 0000000..2f56d6f --- /dev/null +++ b/api-security-schemes/auth_examples/post-products_application_json_401.json @@ -0,0 +1,29 @@ +{ + "http-request": { + "path": "/products", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer dolores" + }, + "body": { + "name": "Diogenes", + "type": "book", + "inventory": 833 + } + }, + "http-response": { + "status": 401, + "body": { + "error": "Loop Detected", + "timestamp": "2026-07-01T12:26:22+05:30", + "status": 401, + "message": "Unauthorized", + "path": "/products" + }, + "status-text": "Unauthorized", + "headers": { + "Content-Type": "application/json" + } + } +} diff --git a/api-security-schemes/docker-compose.yaml b/api-security-schemes/docker-compose.yaml index 94d4cf0..234994e 100644 --- a/api-security-schemes/docker-compose.yaml +++ b/api-security-schemes/docker-compose.yaml @@ -31,6 +31,11 @@ services: working_dir: /usr/src/app environment: APP_BASE_URL: http://order-api:8080 + KEYCLOAK_BASE_URL: http://keycloak:8080 + KEYCLOAK_USER_USERNAME: user1 + KEYCLOAK_USER_PASSWORD: password + KEYCLOAK_ADMIN_USERNAME: admin1 + KEYCLOAK_ADMIN_PASSWORD: password volumes: - .:/usr/src/app - ../license.txt:/specmatic/specmatic-license.txt:ro @@ -51,23 +56,4 @@ services: 90 \ 2 \ "200,401" - - TOKEN_URL="http://keycloak:8080/realms/specmatic/protocol/openid-connect/token" - - echo "Fetching OAuth token from Keycloak..." - OAUTH_TOKEN="$$(curl -fsS -X POST "$${TOKEN_URL}" \ - -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")" - - if [ -z "$${OAUTH_TOKEN}" ] || [ "$${OAUTH_TOKEN}" = "null" ]; then - echo "Failed to fetch access token from Keycloak" - exit 1 - fi - - export OAUTH_TOKEN specmatic test diff --git a/api-security-schemes/keycloak/specmatic-realm.json b/api-security-schemes/keycloak/specmatic-realm.json index 920c1cd..6fb9482 100644 --- a/api-security-schemes/keycloak/specmatic-realm.json +++ b/api-security-schemes/keycloak/specmatic-realm.json @@ -60,6 +60,22 @@ "clientRole" : false, "containerId" : "0b5ab4bf-ef5c-40ee-859c-1871961648cb", "attributes" : { } + }, { + "id" : "2d7a0e5e-9d0f-4d65-9c26-7f6d2b5e6f10", + "name" : "users", + "description" : "Orders RBAC role", + "composite" : false, + "clientRole" : false, + "containerId" : "0b5ab4bf-ef5c-40ee-859c-1871961648cb", + "attributes" : { } + }, { + "id" : "b0c9eb52-3c3e-4d53-8b88-2b2d2a7d1f12", + "name" : "admins", + "description" : "Products RBAC role", + "composite" : false, + "clientRole" : false, + "containerId" : "0b5ab4bf-ef5c-40ee-859c-1871961648cb", + "attributes" : { } }, { "id" : "e884e41c-9e39-4c4a-a499-db7e2c0548c8", "name" : "offline_access", @@ -399,7 +415,29 @@ } ], "disableableCredentialTypes" : [ ], "requiredActions" : [ ], - "realmRoles" : [ "user", "default-roles-specmatic" ], + "realmRoles" : [ "users", "default-roles-specmatic" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "c8d2d2c8-4cb4-4d4c-b1f2-2c5f5e6c7a01", + "createdTimestamp" : 1695470291837, + "username" : "admin1", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "firstName" : "", + "lastName" : "", + "credentials" : [ { + "id" : "2d3b8df2-0dc2-4ed6-a2d7-9d5a0d4fb2f9", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1695470306299, + "secretData" : "{\"value\":\"1A1J63Kp1iAx6XbM5N4TZgK/YpyWKxaCdhjK7YSosh0=\",\"salt\":\"XoLsW5MP5qZmhF8nq4jcgA==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "admins", "default-roles-specmatic" ], "notBefore" : 0, "groups" : [ ] } ], diff --git a/api-security-schemes/specmatic.yaml b/api-security-schemes/specmatic.yaml index c3dc94a..643a72f 100644 --- a/api-security-schemes/specmatic.yaml +++ b/api-security-schemes/specmatic.yaml @@ -4,6 +4,10 @@ systemUnderTest: $ref: "#/components/services/orderService" runOptions: $ref: "#/components/runOptions/orderServiceTest" + data: + examples: + - directories: + - ./auth_examples settings: schemaResiliencyTests: all specmatic: @@ -42,9 +46,6 @@ components: - spec: id: orderApiSpec securitySchemes: - oAuth2AuthCode: - type: oauth2 - token: ${INVALID_OAUTH_TOKEN:OAUTH1234} basicAuth: type: basicAuth token: ${BASIC_AUTH_TOKEN:dXNlcjppbnZhbGlkcGFzcw==}