How to Describe API Security with OpenAPI (OAS): A Practical Guide to Authentication and Authorization in Your Spec

How to Describe API Security with OpenAPI (OAS): A Practical Guide to Authentication and Authorization in Your Spec

Security is one part of API design I refuse to treat as an afterthought, and I refuse to leave it undocumented. If a consumer has to email support to figure out how to authenticate against my API, I’ve already failed at design. In this article, I’m going to walk through exactly how I use OpenAPI’s security features to describe authentication and authorization clearly, precisely, and in a way that both humans and tools can act on.

This builds directly on the reusable components I covered in the previous article, since security schemes live in that same components section.

The Two Layers of API Security in OAS

OpenAPI separates security into two layers, and understanding this separation early saves a lot of confusion:

  1. Security Schemes — the type of authentication mechanism available (API key, OAuth2, bearer token, etc.), defined once in components.securitySchemes.
  2. Security Requirementswhere those schemes are actually required, applied globally or per-operation using the security field.

I always define the scheme first, then decide where it applies.

Security Scheme Types I Use Most

API Key

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

I use API keys most often for simpler, server-to-server integrations — partner APIs, internal microservices, or low-risk public data APIs. The in field can be header, query, or cookie, but I almost always choose header, since API keys in query strings tend to leak into server logs and browser history.

HTTP Bearer (Typically JWT)

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

This is my default for APIs consumed by web or mobile apps where a user logs in and receives a token. The bearerFormat field is purely descriptive — it tells readers what kind of token to expect, even though it’s not enforced by the spec itself.

HTTP Basic

components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic

I use this rarely, mostly for internal tooling or quick prototypes, since basic auth sends credentials on every request and needs HTTPS to be even minimally safe. I almost never recommend it for production public APIs.

OAuth2

components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.example.com/oauth/authorize
          tokenUrl: https://auth.example.com/oauth/token
          refreshUrl: https://auth.example.com/oauth/refresh
          scopes:
            orders:read: Read access to orders
            orders:write: Create and update orders
            orders:admin: Full administrative access to orders

This is the one I spend the most time getting right, because OAuth2 has four different “flows,” and picking the wrong one for your use case creates real security and usability problems:

  • authorizationCode — the standard flow for web and mobile apps where a real user logs in through a browser redirect. This is what I use for consumer-facing apps.
  • clientCredentials — for machine-to-machine communication with no human user involved, like a backend service calling another backend service.
  • implicit — largely deprecated now in favor of authorization code with PKCE; I don’t recommend it for new APIs.
  • password — also discouraged, since it requires the client app to directly handle the user’s username and password, which defeats much of the purpose of OAuth2’s delegation model.
components:
  securitySchemes:
    OAuth2ClientCredentials:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://auth.example.com/oauth/token
          scopes:
            reports:read: Read access to reporting data

OpenID Connect

components:
  securitySchemes:
    OidcAuth:
      type: openIdConnect
      openIdConnectUrl: https://auth.example.com/.well-known/openid-configuration

For APIs built on top of an identity provider that publishes a full OIDC discovery document, this single field lets tools automatically discover the authorization URL, token URL, supported scopes, and more, without me having to redefine all of it manually.

Mutual TLS

components:
  securitySchemes:
    MutualTLS:
      type: mutualTLS

I reach for this in high-security financial or healthcare integrations, where both client and server present certificates to verify each other’s identity. It’s less common, but OAS 3.1 supports describing it explicitly, which I always appreciate when I encounter it in a partner’s API documentation.

Applying Security Requirements

Once schemes are defined, I decide where they apply using the top-level security field (global default) and per-operation security overrides.

Global Security

security:
  - BearerAuth: []

This says: “by default, every operation in this API requires a bearer token.” I set this as the sensible default for almost every private or partner API I design.

Per-Operation Overrides

paths:
  /health:
    get:
      summary: Health check
      security: []
      responses:
        '200':
          description: Service is healthy

  /orders:
    post:
      summary: Create an order
      security:
        - OAuth2: [orders:write]
      responses:
        '201':
          description: Order created

Notice two things here. First, /health explicitly overrides with an empty array security: [], meaning “no authentication required” — I always do this explicitly rather than relying on ambiguity, because a reader shouldn’t have to guess whether a public endpoint is intentionally open or accidentally unsecured. Second, /orders requires the orders:write scope specifically, not just any valid token — this is where scope-based authorization gets described precisely.

Multiple Accepted Schemes (OR Logic)

security:
  - BearerAuth: []
  - ApiKeyAuth: []

Listing multiple entries in the top-level array means “any one of these is acceptable” — a consumer can authenticate with either a bearer token or an API key.

Combined Requirements (AND Logic)

security:
  - BearerAuth: []
    ApiKeyAuth: []

Putting multiple schemes inside the same array entry means both are required simultaneously — useful for APIs that require, say, both a user token and a separate application-level API key.

Documenting Scopes Meaningfully

I’ve reviewed OAuth2 API specs where every scope description just says “Access to X.” That’s not useful. I always try to describe scopes the way I’d explain them to a new partner developer on a call:

scopes:
  orders:read: "View order details, including status and item lists. Does not include customer payment information."
  orders:write: "Create new orders and update their status. Does not permit cancellation or refunds."
  orders:admin: "Full control over orders, including cancellation, refunds, and viewing payment details. Intended for internal support tooling only."

This kind of clarity prevents partners from over-requesting scopes “just in case,” which is itself a security risk (the principle of least privilege, described from the API design side).

Describing Rate Limiting and Abuse Protection

OpenAPI doesn’t have a dedicated rate-limiting keyword, but I always document expected behavior anyway, usually through response headers and a 429 response defined as a reusable component.

components:
  responses:
    TooManyRequests:
      description: Rate limit exceeded. Retry after the time specified in the Retry-After header.
      headers:
        Retry-After:
          schema:
            type: integer
          description: Number of seconds to wait before retrying
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
paths:
  /orders:
    post:
      responses:
        '429':
          $ref: '#/components/responses/TooManyRequests'

Even though rate limiting is enforced outside the spec (usually at a gateway or middleware layer), documenting it inside the OAS document means consumers know to expect and handle it gracefully, instead of being surprised in production.

Describing Sensitive Data Handling

Security in an API isn’t only about who can call an endpoint — it’s also about what data gets exposed. I use a few conventions consistently:

CreditCard:
  type: object
  properties:
    last4:
      type: string
      description: Last four digits of the card number only
    brand:
      type: string
    fullNumber:
      type: string
      writeOnly: true
      description: >
        Full card number, accepted on input only. Never returned
        in any API response.

writeOnly: true is one of the most important, most underused fields for describing security-sensitive data. It tells every consumer and every tool: this field can be sent to the API, but it will never come back in a response. I use this constantly for passwords, full card numbers, and any other secret that should flow in one direction only.

The inverse, readOnly: true, is what I use for server-generated identifiers and timestamps, as I mentioned in the JSON Schema article — it prevents consumers from mistakenly thinking they should (or can) set those fields themselves.

A Full Realistic Example

Let me put it all together into a single coherent security setup, the way I’d actually structure it for a mid-sized partner-facing API.

components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://auth.example.com/oauth/token
          scopes:
            orders:read: "View order details"
            orders:write: "Create and update orders"

security:
  - OAuth2: [orders:read]

paths:
  /health:
    get:
      security: []
      responses:
        '200':
          description: OK

  /orders:
    get:
      summary: List orders
      responses:
        '200':
          description: A list of orders
    post:
      summary: Create an order
      security:
        - OAuth2: [orders:write]
      responses:
        '201':
          description: Order created

This document tells a complete, unambiguous security story: health checks are public, reading orders needs a basic read scope (inherited from the global default), and creating orders needs an explicit write scope.

Common Security Documentation Mistakes

  • Leaving security undefined entirely, forcing consumers to guess whether an endpoint needs auth.
  • Vague scope descriptions that don’t actually explain what access is being granted.
  • Not marking sensitive fields with writeOnly, leading to accidental exposure of secrets in generated response examples.
  • Forgetting to document rate limits and their associated 429 responses.
  • Using implicit or password OAuth2 flows for new APIs, when authorizationCode (with PKCE) or clientCredentials are safer, more modern choices.

Where This Fits Into the Bigger Picture

Documenting security clearly in your OAS file is what lets frontend teams, partners, and security reviewers understand your API’s access model without a single meeting. It also feeds directly into actual security architecture decisions, which I explore much more broadly — beyond just what OAS can express — in the next article in this series, “Designing a Secure API.”

Key Takeaways

  • Separate security schemes (the mechanism) from security requirements (where it’s enforced).
  • Choose the right OAuth2 flow deliberately — authorizationCode for user-facing apps, clientCredentials for machine-to-machine.
  • Always explicitly set security: [] on genuinely public endpoints rather than leaving it ambiguous.
  • Write real, specific scope descriptions, not generic placeholders.
  • Use writeOnly and readOnly to describe the direction sensitive data is allowed to flow.
  • Document expected rate-limiting behavior with a reusable 429 response.

A precisely documented security model in your OpenAPI spec does more than satisfy a checklist — it’s often the first thing a security-conscious partner or reviewer checks before they trust your API at all.

Total
0
Shares

Leave a Reply

Previous Post
Designing a Secure API: A Complete Practical Guide Beyond Just Authentication

Designing a Secure API: A Complete Practical Guide Beyond Just Authentication

Next Post
How to Reuse Components in an OAS Document: Building Maintainable, DRY OpenAPI Specifications

How to Reuse Components in an OAS Document: Building Maintainable, DRY OpenAPI Specifications

Related Posts