Multiple security schemes in OpenAPI: AND vs OR, optional auth, and per-operation overrides
The security keyword is small and surprisingly easy to invert. It is an array of alternatives, where each alternative is itself a set of schemes that must all be satisfied. Miss the distinction between the outer array an
The security keyword is small and surprisingly easy to invert. It is an array of alternatives, where each alternative is itself a set of schemes that must all be satisfied. Miss the distinction between the outer array and the inner object and you either leave a sensitive endpoint protected by a single factor when you required two, or force credentials on an endpoint that was meant to be public. Once you read security as an OR of ANDs, every multi-auth design falls into place.
The rule: an OR of ANDs
security:
- A
- B
means "A or B," either scheme is sufficient.
security:
- [A, B]
written as one requirement object containing both, means "A and B," the caller must satisfy both. In YAML the inner object is a map listing the required schemes and their scopes:
security:
- apiKeyAuth: []
oauth2: [read:orders]
This single array element containing two schemes requires the caller to send both the API key and the OAuth token. To express "either an API key or OAuth," use two array elements:
security:
- apiKeyAuth: []
- oauth2: [read:orders]
| Goal | Shape |
|---|---|
| Any one of several login methods (API key or bearer or OAuth) | Multiple requirement objects, one scheme each |
| Two factors together (API key and bearer) | One requirement object listing both |
| OAuth with scopes | One object, scopes in the array |
| Public, no auth | An empty requirement object security: []
|
Define the schemes once
Schemes are declared under components.securitySchemes; the security keyword only references them:
components:
securitySchemes:
apiKeyAuth:
type: apiKey
in: header
name: X-API-Key
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
oauth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://auth.example.com/authorize
tokenUrl: https://auth.example.com/token
scopes:
'read:orders': Read orders
'write:orders': Create or modify orders
partnerSignature:
type: apiKey
in: header
name: X-Signature
Global default with per-operation overrides
Set a default at the root for the majority of endpoints, then override on the exceptions. An operation-level security replaces the global one entirely (it does not merge), which is the behavior people most often get wrong:
security:
- bearerAuth: []
paths:
/public/status:
get:
summary: Public status page
security: [] # explicitly unauthenticated; overrides the global bearer
...
/webhooks/inbound:
post:
summary: Partner webhook; requires both an API key and an HMAC signature
security:
- apiKeyAuth: []
partnerSignature: []
...
/orders:
get:
summary: Bearer token OR API key
security:
- bearerAuth: []
- apiKeyAuth: []
...
Because operation security replaces rather than extends, an operation that needs the global scheme plus an extra one must restate the global scheme in its own requirement.
Optional auth that changes the response
A common pattern is an endpoint that works anonymously but returns more when authenticated, for example a catalog that shows retail prices to the public and negotiated prices to logged-in partners. This is not "OR" in the security sense; it is genuinely optional authentication. Express it with an empty alternative alongside the authenticated one:
paths:
/products:
get:
summary: Public catalog; authenticated partners see contract pricing
security:
- {}
- bearerAuth: []
- apiKeyAuth: []
responses:
'200':
description: Catalog; partner-only fields are omitted for anonymous callers.
content:
application/json:
schema:
$ref: '#/components/schemas/ProductList'
The empty {} requirement means "no credentials required," and the other entries add the authenticated identities. The response description must say which fields appear only for authenticated callers; the schema can mark those fields optional and document the condition. Do not model optional auth by omitting security entirely while silently reading a token, because then generated clients and agents never know they can send one.
Scoped OAuth and least privilege
For OAuth2 and OpenID Connect, the scope list inside a requirement is itself an AND over scopes: the token must carry every listed scope. Give each operation the narrowest set it needs rather than a blanket scope:
/orders:
post:
security:
- oauth2: ['write:orders']
/orders/{id}:
get:
security:
- oauth2: ['read:orders']
Model read and write as separate scopes so a token used by a read-only integration cannot create orders. When an endpoint accepts either scoped OAuth or a simpler API key, list them as separate requirement objects, and document that the API key may carry different rate limits or permissions.
mTLS and cookie auth
- Mutual TLS uses
type: mutualTLS(OpenAPI 3.0.1+) with a description pointing at the certificate authority and SAN requirements; the actual client-certificate negotiation happens at the transport and is not a header you send. - Cookie/session auth is modeled as
type: apiKey, in: cookie, name: <session cookie>. Note that cookie auth is generally unsuitable for cross-origin and token-based API clients and does not carry the richer metadata of OAuth; prefer bearer tokens for programmatic access and reserve cookies for browser-first endpoints.
What codegen, validators, and AI agents do
- Generators and documentation renderers read the OR-of-ANDs structure to produce the right auth UI: a choice of methods for separate requirement objects, and multiple required inputs when one object lists several schemes. Getting the nesting wrong renders a misleading sign-in flow.
- Runtime security tooling and gateways can use the spec to enforce that a webhook route actually requires both the key and the signature; a spec that models them as OR would fail open.
- Contract tests should assert the matrix: a public endpoint returns 200 with no credentials, an AND endpoint returns 401 with only one factor, an OR endpoint accepts either, and the optional-auth endpoint omits partner fields anonymously and includes them with a token.
- An AI agent calling the API decides what credentials to send from
security. An accurate spec means it authenticates once with the narrowest sufficient scheme; a confused OR/AND model makes it either give up or send credentials to a public route.
Checklist
- Read
securityas an OR of requirement objects, where each object is an AND of schemes. - Use separate requirement objects for "any one method" and one multi-scheme object for "all factors required."
- Declare every scheme once under
components.securitySchemes; reference it insecurity. - Remember operation-level
securityreplaces the global default; restate inherited schemes when you add one. - Make public routes explicit with
security: []; model optional-auth with an empty alternative plus authenticated ones and document conditional fields. - Apply least-privilege, separate OAuth scopes per operation.
- Test the full auth matrix (anonymous, single factor, all factors, wrong scope) and confirm renderers show the intended flow.
Get the OR-of-ANDs structure right and the spec, the generated clients, the gateway, and the AI agents calling your API all enforce exactly the authentication you intended.
You can model multi-scheme and optional auth, generate clients that send the right credentials, and test the full auth matrix in one local-first workspace, right in your browser. For how to define each underlying scheme (Bearer, API keys, OAuth 2, mTLS), see documenting API authentication in OpenAPI 3.1.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.