Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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):

ResultStatus
No credentials, or wrong ones401 with a WWW-Authenticate challenge (Basic realm="mock" or Bearer realm="mock") when the scheme has one
A valid token without a required scope403, 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.securitySchemesAccepted credentials
apiKey (in: header, query or cookie)One of security.api_key.keys (default demo-key) under the scheme's own name
http basicA user and password from security.basic.users (default demo / demo)
http bearerA token from security.bearer.tokens (default demo-token), or a JWT from the built-in identity provider
oauth2, openIdConnectA 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
mutualTLSA 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.modeAccepted credentials
api_keyOne of api_key.keys in the header api_key.header (default x-api-key)
basicA user from basic.users
bearerA token from bearer.tokens, or a JWT from the built-in identity provider
jwtOnly 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

SettingDefaultLimit
security.api_key.keys["demo-key"]50 keys, 1 to 512 characters
security.api_key.header (fixed api_key mode)x-api-keya 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.audienceunset (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