Engineering

OpenAPI security best practices for production APIs

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 of APIError) 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).
  • securitySchemes includes a description for every scheme telling customers where the credential comes from.
  • No in: query API keys.
  • OAuth scopes: are named for capability (write:messages), not role (admin).
  • bearerFormat is 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.