OpenAPI 3.x has a real security model: securitySchemes in components, security: on operations, scopes for OAuth flows, and conventions for keys, certificates, and bearer tokens. Modeled correctly, your spec produces SDKs that handle auth idiomatically, docs that explain auth clearly, and operations that pass an audit. Modeled badly, the spec produces SDKs whose customers paste keys into the wrong header and miss scope requirements until production.
This is the short, practical version of how to do it well.
The five security schemes you'll actually use
1. API key (type: apiKey)
The simplest scheme. The key lives in a header, query parameter, or cookie.
components:
securitySchemes:
ApiKey:
type: apiKey
in: header
name: X-API-Key
description: |
Sandbox keys begin with `sb_`. Production keys begin with `live_`.
Rotate at https://app.example.com/keys.
Use in: header unless you have a legacy reason for query parameters. Query keys leak into logs and Referer headers.
2. Bearer token (type: http, scheme: bearer)
Used for short-lived JWTs and PATs. Standard, recognized by every client library.
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: Personal access token or service-account token.
3. OAuth 2.0 (type: oauth2)
For three-party flows where end-users grant your customers access to their data.
components:
securitySchemes:
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: https://app.example.com/oauth/authorize
tokenUrl: https://api.example.com/oauth/token
scopes:
"read:messages": Read message history
"write:messages": Send messages
"manage:webhooks": Configure webhooks
Define real scopes — not just read and write. Generated SDKs and docs use the scope descriptions; vague names produce vague docs.
4. Mutual TLS (type: mutualTLS)
For service-to-service. Common in fintech and high-compliance environments.
components:
securitySchemes:
MutualTLS:
type: mutualTLS
description: Client certificate provisioned via the production dashboard.
OpenAPI 3.1 added mutualTLS as a first-class type. In 3.0 you faked it with type: http; scheme: bearer and out-of-band docs.
5. OpenID Connect (type: openIdConnect)
When you sit behind an IdP. Reference the discovery URL and let SDKs read scopes from there.
components:
securitySchemes:
OIDC:
type: openIdConnect
openIdConnectUrl: https://idp.example.com/.well-known/openid-configuration
Applying schemes to operations
Define schemes once. Apply at the operation level for endpoints that differ. Apply at the top level for the default:
# Default: every operation requires the API key.
security:
- ApiKey: []
paths:
/webhooks/incoming:
post:
# This endpoint is called by external services. Override with no auth.
security: []
...
/admin/users:
delete:
# Admin endpoints need an additional scope.
security:
- OAuth2: ["admin:users"]
Operation-level security: replaces the top-level default for that operation. It does not merge. The empty array (security: []) means "explicitly unauthenticated."
What good security modeling produces
When the spec is clean:
- Generated SDKs handle auth idiomatically. Bloom's TypeScript SDK reads the API key from a constructor option or env var; the Python SDK does the same. Customers don't think about which header to set.
- Docs explain scopes inline. Each operation page shows which scopes are required, with the scope descriptions you supplied.
- Audits pass. SOC 2 reviewers can read the spec and see exactly which endpoints require which authentication. Field-level scope requirements are visible.
The gotchas that bite in production
1. Putting the API key in the URL.
https://api.example.com?api_key=sk_live_… leaks into:
- Web server access logs
- CDN logs
- Browser Referer headers
- Bug reports and stack traces
If your spec uses in: query for an API key, change it. The "we always rotate keys quickly" argument is not enough; rotation is a recovery action, not a prevention.
2. Marking everything security: [].
If your top-level default is empty and every operation needs auth, every operation needs security: explicitly. Some tools (older versions of Swagger UI, some validators) interpret missing operation-level security: as "anonymous allowed" depending on the top-level shape. Be explicit.
3. Not modeling scopes on read endpoints.
If your OAuth scopes only apply to writes, that's a security model design decision. State it. Don't let it be implicit — readers of your spec (humans and codegen) will assume scopes apply where you don't say.
4. Mixing bearer and apiKey for the same surface.
If you accept either, model it as security: [BearerAuth: [], ApiKey: []] — an OR. If you accept both required, use security: [BearerAuth: [], ApiKey: []] as two entries (which is OR in OpenAPI). A required AND of both auth methods is unusual; check whether you really mean it.
5. Forgetting bearerFormat.
bearerFormat: JWT (or bearerFormat: opaque) is a documentation-only field, but generated SDKs and docs use it. Without it, clients get a confusing experience.
6. Documenting key/token format only in prose.
Use description on each scheme to tell customers:
- Where they get the credential
- The expected format (prefix, length, structure)
- How rotation works
- The difference between sandbox and production credentials
If a customer can't answer "where does this key come from" by reading the spec, the spec is incomplete.
How Bloom-generated SDKs handle auth
The SDKs Bloom generates from a security-aware spec:
- Read credentials from constructor options first, falling back to env vars derived from your
clientClassName(e.g.ACME_API_API_KEY,ACME_API_API_SECRET). - Use the correct header (or query, or cookie) as the spec declares.
- Throw a typed
AuthenticationError(the 401 subclass ofAPIError) when credentials are missing or rejected. - Document the env var names in the generated README.
If your spec models auth correctly, the SDK behaves correctly with zero hand-coding.
Spec audit checklist
- Every authenticated operation has a clear
security:entry (top-level default or operation-level override). -
securitySchemesincludes adescriptionfor every scheme telling customers where the credential comes from. - No
in: queryAPI keys. - OAuth
scopes:are named for capability (write:messages), not role (admin). -
bearerFormatis set for HTTP bearer schemes. - Unauthenticated endpoints are explicitly
security: [], not just missing the field. - Scope requirements on write endpoints are tighter than on read endpoints (or you've documented why not).
If your spec passes that checklist, an SDK generated from it (Bloom, Stainless, OpenAPI Generator) will hand customers an auth experience they don't have to think about.