Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 52 additions & 23 deletions api-security-schemes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -16,18 +16,17 @@ 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.

## Security Schemes

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.

Expand All @@ -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:
Expand All @@ -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.

Expand All @@ -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==}
Expand All @@ -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

Expand All @@ -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:

Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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.
24 changes: 24 additions & 0 deletions api-security-schemes/auth_examples/delete-orders-id_401.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
24 changes: 24 additions & 0 deletions api-security-schemes/auth_examples/delete-products-id_401.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
24 changes: 24 additions & 0 deletions api-security-schemes/auth_examples/get-orders-id_401.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
28 changes: 28 additions & 0 deletions api-security-schemes/auth_examples/get-orders_401.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
24 changes: 24 additions & 0 deletions api-security-schemes/auth_examples/get-product-id_401.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
27 changes: 27 additions & 0 deletions api-security-schemes/auth_examples/get-products_401.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Loading