Mocked security
A mock can enforce authentication in front of its operations, so a gateway's auth
plugins (key injection, token exchange, OAuth client credentials, mTLS to the
upstream) have something real to satisfy. Security is off by default, even
when the spec declares securitySchemes; switch it on per mock:
{"security": {"enabled": true}}
in the create body, a PATCH (then deploy), rustybin.yaml
or the console's Security tab. Failures are answered before request validation
and response selection, as application/problem+json, and captured with their
reason (auth in the capture, outcome unauthorized or forbidden):
| Result | Status |
|---|---|
| No credentials, or wrong ones | 401 with a WWW-Authenticate challenge (Basic realm="mock" or Bearer realm="mock") when the scheme has one |
| A valid token without a required scope | 403, detail: "insufficient scope (needs ...)" |
Spec mode
security.mode: spec (the default) honours the spec: each operation's security
(or the document's top-level one) is a list of alternatives, any one of which is
enough, and each alternative may combine several schemes, all of which are needed.
security: [] on an operation makes it open.
Scheme in components.securitySchemes | Accepted credentials |
|---|---|
apiKey (in: header, query or cookie) | One of security.api_key.keys (default demo-key) under the scheme's own name |
http basic | A user and password from security.basic.users (default demo / demo) |
http bearer | A token from security.bearer.tokens (default demo-token), or a JWT from the built-in identity provider |
oauth2, openIdConnect | A JWT from the built-in identity provider whose scope (or scp) claim holds every scope the requirement lists; a token from security.bearer.tokens counts as having every scope |
mutualTLS | A client certificate from the demo CA: on the HTTPS listener, or forwarded by a gateway in the RUSTYBIN_MTLS_IN_HEADER header |
JWTs are verified with the identity provider's RS256 key (signature and expiry);
security.jwt.issuer and security.jwt.audience add iss and aud checks. Keys,
passwords and tokens are compared in constant time. Schemes of other types are not
enforced (a spec warning names them).
The built-in sample declares apiKey (header X-API-Key) or bearer on its write
operations:
PATCH {{base_url}}/_rustybin/mocks/petstore
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"security": {"enabled": true}, "deploy": true}
HTTP 200
[Asserts]
jsonpath "$.settings.security.enabled" == true
jsonpath "$.settings.security.mode" == "spec"
jsonpath "$.credentials.api_key.keys" contains "demo-key"
jsonpath "$.credentials.bearer.tokens" contains "demo-token"
jsonpath "$.credentials.token_endpoint" == "/oauth/token"
# createPet declares `apiKey` OR `bearer`.
POST {{base_url}}/mock/petstore/v1/pets
Content-Type: application/json
{"name": "Rex"}
HTTP 401
[Asserts]
header "Content-Type" == "application/problem+json"
header "WWW-Authenticate" == "Bearer realm=\"mock\""
jsonpath "$.detail" == "missing API key (header X-API-Key); missing bearer token"
jsonpath "$.operation" == "createPet"
# listPets declares no security: still open.
GET {{base_url}}/mock/petstore/v1/pets
HTTP 200
POST {{base_url}}/mock/petstore/v1/pets
X-API-Key: demo-key
Content-Type: application/json
{"name": "Rex"}
HTTP 201
POST {{base_url}}/mock/petstore/v1/pets
Authorization: Bearer demo-token
Content-Type: application/json
{"name": "Rex"}
HTTP 201
POST {{base_url}}/mock/petstore/v1/pets
X-API-Key: wrong-key
Content-Type: application/json
{"name": "Rex"}
HTTP 401
[Asserts]
jsonpath "$.detail" contains "invalid API key"
GET {{base_url}}/_rustybin/mocks/petstore/requests?limit=1
HTTP 200
[Asserts]
jsonpath "$.requests[0].outcome" == "unauthorized"
jsonpath "$.requests[0].auth" startsWith "failed: invalid API key"
PATCH {{base_url}}/_rustybin/mocks/petstore
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"security": {"enabled": false}, "deploy": true}
HTTP 200
POST {{base_url}}/mock/petstore/v1/pets
Content-Type: application/json
{"name": "Rex"}
HTTP 201
OAuth 2.0 and OpenID Connect with the built-in IdP
Tokens come from the instance's own identity provider: POST /oauth/token with the
demo client rustybin / secret and the client_credentials grant (or any other
grant it supports) returns an RS256 JWT carrying the requested scope. When
security is on, the served spec (/mock/{name}/_rustybin/openapi.json) points the
oauth2 flows' tokenUrl, refreshUrl and authorizationUrl, and an
openIdConnect scheme's openIdConnectUrl, at that identity provider, so a gateway
or API client that imports the spec can really obtain a token.
This mock combines an oauth2 scheme with scopes, an API key in the query, an
open health check and a mutualTLS operation:
POST {{base_url}}/_rustybin/mocks
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"name": "docs-secure",
"security": {"enabled": true},
"spec": {
"openapi": "3.0.3",
"info": {"title": "Orders", "version": "1.0"},
"security": [{"oauth": ["orders:read"]}],
"paths": {
"/health": {"get": {"operationId": "health", "security": [],
"responses": {"200": {"description": "Up", "content": {"application/json": {"example": {"status": "up"}}}}}}},
"/orders": {
"get": {"operationId": "listOrders",
"responses": {"200": {"description": "Orders", "content": {"application/json": {"example": [{"orderId": 1}]}}}}},
"post": {"operationId": "createOrder", "security": [{"oauth": ["orders:write"]}],
"responses": {"201": {"description": "Created", "content": {"application/json": {"example": {"orderId": 2}}}}}}
},
"/orders/{orderId}": {"get": {"operationId": "getOrder",
"security": [{"apiKey": []}, {"oauth": ["orders:read"]}],
"parameters": [{"name": "orderId", "in": "path", "required": true, "schema": {"type": "integer"}}],
"responses": {"200": {"description": "An order", "content": {"application/json": {"example": {"orderId": 1}}}}}}},
"/reports": {"get": {"operationId": "getReport", "security": [{"clientCert": []}],
"responses": {"200": {"description": "A report", "content": {"application/json": {"example": {"rows": 3}}}}}}}
},
"components": {"securitySchemes": {
"apiKey": {"type": "apiKey", "in": "query", "name": "api_key"},
"oauth": {"type": "oauth2", "flows": {"clientCredentials": {
"tokenUrl": "https://idp.example.com/oauth/token",
"scopes": {"orders:read": "Read orders", "orders:write": "Create orders"}}}},
"clientCert": {"type": "mutualTLS"}
}}
}
}
HTTP 201
[Asserts]
jsonpath "$.security.enabled" == true
jsonpath "$.credentials.schemes[*].type" contains "oauth2"
GET {{base_url}}/mock/docs-secure/_rustybin/openapi.json
HTTP 200
[Asserts]
jsonpath "$.components.securitySchemes.oauth.flows.clientCredentials.tokenUrl" == "{{base_url}}/oauth/token"
GET {{base_url}}/mock/docs-secure/orders
HTTP 401
[Asserts]
header "WWW-Authenticate" == "Bearer realm=\"mock\""
POST {{base_url}}/oauth/token
[BasicAuth]
rustybin: secret
[Form]
grant_type: client_credentials
scope: orders:read
HTTP 200
[Captures]
read_token: jsonpath "$.access_token"
GET {{base_url}}/mock/docs-secure/orders
Authorization: Bearer {{read_token}}
HTTP 200
# createOrder needs orders:write.
POST {{base_url}}/mock/docs-secure/orders
Authorization: Bearer {{read_token}}
HTTP 403
[Asserts]
jsonpath "$.detail" == "insufficient scope (needs orders:write)"
POST {{base_url}}/oauth/token
[BasicAuth]
rustybin: secret
[Form]
grant_type: client_credentials
scope: orders:read orders:write
HTTP 200
[Captures]
write_token: jsonpath "$.access_token"
POST {{base_url}}/mock/docs-secure/orders
Authorization: Bearer {{write_token}}
HTTP 201
# getOrder: an API key in the query OR a token with orders:read.
GET {{base_url}}/mock/docs-secure/orders/7?api_key=demo-key
HTTP 200
GET {{base_url}}/mock/docs-secure/orders/7
HTTP 401
[Asserts]
jsonpath "$.detail" == "missing API key (query api_key); missing bearer token"
# health has `security: []` in the spec.
GET {{base_url}}/mock/docs-secure/health
HTTP 200
# run.sh sets RUSTYBIN_MTLS_IN_HEADER=X-Client-Cert, as a gateway that
# terminates mTLS would forward the certificate.
GET {{base_url}}/auth/mtls/get-client-cert
HTTP 200
[Captures]
client_cert: jsonpath "$.cert_pem" urlEncode
GET {{base_url}}/mock/docs-secure/reports
HTTP 401
[Asserts]
jsonpath "$.detail" == "no client certificate from the demo CA"
GET {{base_url}}/mock/docs-secure/reports
X-Client-Cert: {{client_cert}}
HTTP 200
Fixed modes
For specs that declare no security, or to demo one scheme regardless of the spec,
pick a fixed mode: it protects every operation (the spec's requirements, and
security: [], are ignored) except the ones marked public.
security.mode | Accepted credentials |
|---|---|
api_key | One of api_key.keys in the header api_key.header (default x-api-key) |
basic | A user from basic.users |
bearer | A token from bearer.tokens, or a JWT from the built-in identity provider |
jwt | Only a JWT from the built-in identity provider (with the jwt.issuer / jwt.audience checks when set) |
# A fixed mode ignores the spec's requirements: every operation that is not
# public needs these credentials.
PATCH {{base_url}}/_rustybin/mocks/docs-secure
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"security": {"enabled": true, "mode": "basic", "basic": {"users": {"alice": "wonderland"}}}, "deploy": true}
HTTP 200
GET {{base_url}}/mock/docs-secure/health
HTTP 401
[Asserts]
header "WWW-Authenticate" == "Basic realm=\"mock\""
GET {{base_url}}/mock/docs-secure/health
[BasicAuth]
alice: wonderland
HTTP 200
GET {{base_url}}/mock/docs-secure/orders
HTTP 200
PATCH {{base_url}}/_rustybin/mocks/docs-secure
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"security": {"enabled": true, "mode": "api_key", "api_key": {"header": "apikey", "keys": ["k-123", "k-456"]}}, "deploy": true}
HTTP 200
GET {{base_url}}/mock/docs-secure/orders/7
apikey: k-456
HTTP 200
# jwt: only tokens signed by the built-in IdP (static bearer tokens are refused),
# here also with a required audience.
PATCH {{base_url}}/_rustybin/mocks/docs-secure
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"security": {"enabled": true, "mode": "jwt", "jwt": {"audience": "rustybin"}}, "deploy": true}
HTTP 200
GET {{base_url}}/mock/docs-secure/orders/7
Authorization: Bearer demo-token
HTTP 401
[Asserts]
jsonpath "$.detail" startsWith "invalid token"
GET {{base_url}}/mock/docs-secure/orders/7
Authorization: Bearer {{read_token}}
HTTP 200
Demo credentials
| Setting | Default | Limit |
|---|---|---|
security.api_key.keys | ["demo-key"] | 50 keys, 1 to 512 characters |
security.api_key.header (fixed api_key mode) | x-api-key | a valid header name |
security.basic.users | {"demo": "demo"} | 50 users |
security.bearer.tokens | ["demo-token"] | 50 tokens, 1 to 512 characters |
security.jwt.issuer, security.jwt.audience | unset (not checked) |
Setting security replaces the whole section, so list every credential you want
to keep. The mock's details (GET /_rustybin/mocks/{name}, the console's Security
tab) show them under credentials, with a hint per scheme and the token endpoint;
basic passwords are not shown there. In rustybin.yaml, write secrets as
${ENV_VAR} references (environment variables).
The deploy report's sample requests and the console's "Try it" already carry the
demo credentials.
Public operations
"public": true in an operation's settings switches mocked security off for that
operation in every mode, for health checks and other open endpoints:
PUT {{base_url}}/_rustybin/mocks/docs-secure/operations/listOrders
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"public": true}
HTTP 200
POST {{base_url}}/_rustybin/mocks/docs-secure/deploy
Authorization: Bearer {{admin_token}}
HTTP 200
GET {{base_url}}/mock/docs-secure/orders
HTTP 200