Authentication is the part of an API contract that consumers hit first and that documentation most often gets quietly wrong. A security scheme that is defined but never referenced, a bearer token modeled as an ordinary header, or OAuth scopes that do not match the authorization server all produce a spec that renders nicely and fails at first request. OpenAPI 3.1 gives you a precise vocabulary for the mechanisms in common use, including mutual TLS; the discipline is in applying it consistently.

Declare schemes once under components

Every authentication mechanism is a named entry under components.securitySchemes. The five types cover real-world deployments:

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

    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key

    clientCredentials:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://auth.example.com/oauth2/token
          scopes:
            orders:read: Read orders
            orders:write: Create and modify orders

    userOAuth:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.example.com/oauth2/authorize
          tokenUrl: https://auth.example.com/oauth2/token
          refreshUrl: https://auth.example.com/oauth2/token
          scopes:
            orders:read: Read orders
            orders:write: Create and modify orders

    oidcAuth:
      type: openIdConnect
      openIdConnectUrl: https://auth.example.com/.well-known/openid-configuration

    mtlsAuth:
      type: mutualTLS
      description: >-
        Client certificate required; accepted issuers and subject DN rules are
        published in the onboarding guide.

A few details matter. For JWT bearer tokens, use type: http with scheme: bearer and bearerFormat: JWT; do not model Authorization as a header parameter, which hides it from tooling and code generators. For API keys, state the location explicitly; keys in in: query leak into logs and browser history and should be avoided for new APIs in favor of a header or cookie. For OpenID Connect, the discovery URL lets clients fetch endpoints and signing keys themselves.

Mutual TLS became a first-class scheme in OpenAPI 3.1 with type: mutualTLS. The spec deliberately has no structured fields for accepted CAs or subject rules; put those in the description and link your onboarding documentation.

OAuth in 2026: prefer authorization code and client credentials

The oauth2 object can still describe the legacy implicit and password flows, but current OAuth 2.1 practice removes them. For a user-delegated client, use the authorization code flow with PKCE (the authorizationCode entry above). For service-to-service calls with no interactive user, use the client credentials flow. Document the scopes the API actually enforces, because generated clients and agent tooling request scopes from this list; a scope that exists only in the spec produces consent screens and tokens that the server rejects.

Apply schemes globally, then override per operation

Defining a scheme does nothing until it is referenced. Set a default at the root with the security keyword, and override it on operations that differ:

security:
  - bearerAuth: []

paths:
  /orders:
    get:
      security:
        - bearerAuth: [orders:read]
        - clientCredentials: [orders:read]
      responses:
        "200": { description: OK }
  /public/status:
    get:
      security: []
      responses:
        "200": { description: Public health and status }

The combination rules are frequently misunderstood. security is an array of alternatives; each array entry is an object whose entries must all be satisfied. So:

  • [ { bearerAuth: [] } ] means bearer is required.
  • [ { bearerAuth: [] }, { apiKeyAuth: [] } ] means bearer or an API key is accepted.
  • [ { mtlsAuth: [], bearerAuth: [] } ] means a client certificate and a bearer token are both required.

An empty array security: [] on an operation explicitly means no authentication, which is how you publish health checks and public catalog endpoints without them inheriting the global requirement. Forgetting this override is why public endpoints incorrectly show a lock icon in generated docs.

Document the failure responses, not just the happy path

Authentication and authorization failures are part of the contract and should be documented with the distinction between them:

  • 401 Unauthorized means the request is unauthenticated: missing, malformed, or expired credentials. The response can include a WWW-Authenticate header.
  • 403 Forbidden means the caller is authenticated but not permitted for this operation or scope.
      responses:
        "401":
          description: Missing or invalid token
          headers:
            WWW-Authenticate:
              schema: { type: string }
              example: Bearer error="invalid_token"
        "403":
          description: Authenticated but missing the required scope

Modeling these as a reusable components.responses entry keeps them consistent and tells consumers whether to refresh a token or surface a permission error.

The mistakes that show up in review

  • Defined and never applied. A scheme sits in components.securitySchemes with no root or operation security, so the whole API renders as anonymous.
  • Token as a header parameter. This bypasses every auth-aware feature and double-documents the Authorization header.
  • Cookie auth without the caveats. Cookies participate in CORS and credential policies; document SameSite, Secure, and the allowed origins rather than assuming a header client.
  • Stale flows and scopes. Token URLs that return 404 and scopes the server does not recognize are worse than no documentation.
  • No public-endpoint override. Everything inherits global auth, including webhooks and health checks that are genuinely anonymous.
  • Silent AND/OR confusion. Picking the wrong array nesting either demands two tokens from ordinary clients or accepts weak auth where strong auth was intended.
  • Secrets in examples. Never put a real JWT or API key in an example; use an obviously fake placeholder.

Agents and machine clients raise the bar

Agent and MCP clients read the security section to decide how to obtain and refresh tokens without a human present, which makes accuracy more valuable than prose. For service-to-service agent traffic, client credentials or mutual TLS give a clean, non-interactive identity; user-delegated agent actions should use authorization code with PKCE and the narrowest scopes that work. Turning an API into MCP tools inherits exactly these schemes, so an inaccurate security section propagates straight into the agent's call path; the mapping is covered in turning an OpenAPI spec into an MCP server, and the agent-facing context argument is in serving the spec to coding agents.

Authentication documentation is worth linting like schemas: verify every scheme is applied, every protected operation declares a failure response, and the OAuth metadata matches the live server. The Powerduck workspace lets you design and debug these schemes against the same local spec used for docs and mocks; the online demo shows the workflow.