API Design · Lesson 5

OpenAPI Security Schemes Explained

Model bearer tokens, API keys, OAuth 2.0, and OpenID Connect in OpenAPI with Springdoc and Express examples, overrides, tests, and enforcement boundaries.

Quick answer

OpenAPI components.securitySchemes defines reusable descriptions of how clients present credentials. A security array applies those schemes globally or to one operation. Separate objects in the array mean OR; multiple scheme names inside one object mean AND. OAuth 2.0 and OpenID Connect requirement values list required scopes.

Use type: http with scheme: bearer for bearer tokens, type: apiKey for named header/cookie/query credentials, type: oauth2 for explicit flows, and type: openIdConnect for discovery. These declarations power documentation and client tooling; Spring Security or Express middleware must still authenticate and authorize runtime requests.

Threat model

An accurate contract reduces integration mistakes: clients know where credentials belong, reviewers can see required scopes, generators can expose authorization controls, and tests can detect undocumented public operations.

An OpenAPI document does not:

  • validate a token or API key;
  • enforce scopes, roles, issuer, audience, or expiry;
  • keep examples and generated documentation secret;
  • make an obsolete OAuth flow safe;
  • prevent code and specification drift.

A dangerous failure mode is “documented as protected, implemented as public.” The inverse—implemented protection missing from the document—causes broken clients and unsafe manual workarounds. Treat the specification and runtime policy as two artifacts that must agree.

Security Scheme Object versus Security Requirement

A scheme describes credential transport and protocol metadata:

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

A requirement applies it:

security:
  - bearerAuth: []

bearerFormat is a documentation hint; it does not make every bearer token a JWT or validate one. Scheme component names such as bearerAuth are local identifiers, not headers.

Bearer tokens and API keys

A common pair:

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Access token issued by the production identity service.
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Partner key; send only over HTTPS.

Do not place real tokens or keys in examples. Header API keys are usually preferable to query keys because URLs leak into history, analytics, referrers, and logs. The description can state rotation and environment expectations without exposing internal secrets.

Basic authentication can be described with type: http and scheme: basic, but documentation support is not a recommendation. Select an authentication method based on the real threat model.

OAuth 2.0 Authorization Code

Declare the provider endpoints and available scopes:

components:
  securitySchemes:
    oauth:
      type: oauth2
      description: User-delegated access with Authorization Code and PKCE.
      flows:
        authorizationCode:
          authorizationUrl: https://identity.example.test/oauth2/authorize
          tokenUrl: https://identity.example.test/oauth2/token
          refreshUrl: https://identity.example.test/oauth2/token
          scopes:
            orders:read: Read orders for the signed-in account.
            orders:write: Create and modify orders.

OpenAPI describes the flow but does not express every PKCE runtime parameter or validate state. Explain those requirements in operation and security documentation and implement them in the client. Avoid implicit and password flows for new systems merely because the schema supports them for compatibility.

Apply scopes:

paths:
  /orders:
    get:
      security:
        - oauth:
            - orders:read
    post:
      security:
        - oauth:
            - orders:write

The resource server must verify that a trusted issuer granted the required scope for the intended audience.

OpenID Connect

Use discovery when clients should learn provider metadata from an OpenID Connect issuer:

components:
  securitySchemes:
    oidc:
      type: openIdConnect
      openIdConnectUrl: https://identity.example.test/.well-known/openid-configuration

The discovery document is metadata, not a credential. Runtime code still validates issuer, signature, audience, expiry, nonce where applicable, and authorization policy. Do not use an arbitrary user-supplied discovery URL.

Global requirements and operation overrides

A root requirement applies by default:

security:
  - bearerAuth: []

An operation can replace it:

paths:
  /health:
    get:
      security: []

An empty array means no security requirement applies to that operation. This is appropriate for a deliberately public health endpoint, but it deserves review because a mistaken empty array publishes access.

Optional authentication uses an empty requirement object as one alternative:

security:
  - {}
  - bearerAuth: []

That says anonymous OR bearer. Do not confuse security: [] with security: [{}]; both permit unauthenticated access, but the latter explicitly models an anonymous alternative alongside other entries.

OR and AND semantics

Separate array items mean OR:

security:
  - bearerAuth: []
  - apiKeyAuth: []

The caller may satisfy bearer or API key.

Multiple schemes in one object mean AND:

security:
  - bearerAuth: []
    apiKeyAuth: []

The caller must satisfy both. This distinction is easy to miss and can produce generated documentation that promises a weaker or stronger policy than runtime code.

Scopes inside one OAuth scheme are all required for that requirement:

security:
  - oauth:
      - orders:read
      - customer:read

Java with Springdoc

Springdoc can generate the component and apply requirements with annotations:

@Configuration
@OpenAPIDefinition(
    info = @Info(title = "Orders API", version = "1.0.0"),
    security = @SecurityRequirement(name = "bearerAuth")
)
@SecurityScheme(
    name = "bearerAuth",
    type = SecuritySchemeType.HTTP,
    scheme = "bearer",
    bearerFormat = "JWT",
    description = "Access token issued by the production identity service."
)
class OpenApiConfiguration {
}

Override a public operation:

@RestController
class HealthController {
    @Operation(
        summary = "Service health",
        security = {}
    )
    @GetMapping("/health")
    Map<String, String> health() {
        return Map.of("status", "ok");
    }
}

Runtime enforcement is separate:

@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/health").permitAll()
            .requestMatchers(HttpMethod.GET, "/orders/**")
                .hasAuthority("SCOPE_orders:read")
            .requestMatchers(HttpMethod.POST, "/orders/**")
                .hasAuthority("SCOPE_orders:write")
            .anyRequest().authenticated())
        .oauth2ResourceServer(resource -> resource.jwt(Customizer.withDefaults()))
        .build();
}

The @SecurityRequirement improves the contract; SecurityFilterChain protects the application.

Node.js with Express

An Express application can keep an OpenAPI object beside route code:

export const document = {
  openapi: "3.1.0",
  info: { title: "Orders API", version: "1.0.0" },
  components: {
    securitySchemes: {
      bearerAuth: {
        type: "http",
        scheme: "bearer",
        bearerFormat: "JWT"
      },
      apiKeyAuth: {
        type: "apiKey",
        in: "header",
        name: "X-API-Key"
      }
    }
  },
  security: [{ bearerAuth: [] }],
  paths: {
    "/health": {
      get: {
        security: [],
        responses: { "200": { description: "Healthy" } }
      }
    }
  }
};

Express still needs middleware:

app.get("/health", health);
app.get(
  "/orders",
  authenticateBearer,
  requireScopes("orders:read"),
  listOrders
);
app.post(
  "/partner/import",
  authenticateApiKey,
  requirePartnerPermission("orders:write"),
  importOrders
);

Centralize the mapping between route policy and OpenAPI requirements where tooling allows it, but do not weaken runtime boundaries to make generation convenient.

Combining request signing

A private HMAC scheme can be represented as an API key-like set of headers only imperfectly; OpenAPI’s apiKey scheme names one credential location. Document the additional timestamp, nonce, and signature headers as parameters, or use a documented extension supported by your tooling.

components:
  securitySchemes:
    signingKeyId:
      type: apiKey
      in: header
      name: X-Key-Id

Then explain the complete protocol and link to API Request Signing Explained. Do not imply that the key ID alone authenticates the request.

Failure responses

Document stable 401 and 403 responses:

components:
  responses:
    Unauthorized:
      description: Missing or invalid credentials.
    Forbidden:
      description: Valid identity lacks required permission.

401 means authentication is missing or invalid; 403 means an authenticated identity is not permitted. OAuth bearer APIs can also use the standards-defined WWW-Authenticate response. Do not include token contents, expected API keys, or internal validation traces in examples.

Validation and drift testing

Validate the document against the chosen OpenAPI version. Then compare it with runtime routes:

  • every protected route has a security requirement;
  • every explicitly public override is reviewed;
  • every required OAuth scope exists in the scheme;
  • every referenced scheme name is defined;
  • OR/AND layout matches middleware behavior;
  • 401 and 403 responses are documented;
  • generated docs contain no secrets;
  • production server URLs and issuer metadata are correct for the environment.

Contract tests can call each route without credentials, with malformed credentials, with valid insufficient credentials, and with valid permitted credentials. Specification linting alone cannot prove enforcement.

Add a policy-diff check during review. Extract the normalized method and path pairs from the built OpenAPI document, compare them with the router inventory, and flag protected runtime routes that have no contract entry. For routes present in both, compare the scheme name and required scopes with the middleware configuration. Generated specifications often drift when annotations are copied between controllers or a new route bypasses the shared registration helper.

Documentation visibility also needs a decision. Public API descriptions should contain only public server URLs, scope names, and safe examples. Internal descriptions may reveal administrative paths or identity topology, so protect distribution without placing credentials inside the document. Filtering documentation is not a substitute for protecting the actual route.

Common mistakes

Defining a scheme but never applying it. Components are reusable definitions; security activates them.

Assuming documentation enforces security. Runtime filters and middleware do that.

Reversing OR and AND. Separate objects are alternatives; names inside one object are cumulative.

Using real credentials in examples. Documentation is broadly copied and cached.

Declaring every route globally with no reviewed overrides. Public health or callback routes need explicit decisions.

Listing scopes the resource server does not enforce. This creates false assurance.

Treating bearerFormat: JWT as validation. It is only a hint.

Choosing obsolete OAuth flows because the schema permits them. Compatibility is not endorsement.

Practical checklist

  • Choose the exact OpenAPI version and validate against it.
  • Define descriptive, stable scheme component names.
  • Apply schemes globally or per operation.
  • Review every public security: [].
  • Model OR with separate objects and AND inside one object.
  • List real OAuth scopes and enforce them at runtime.
  • Keep credentials out of examples and generated UI defaults.
  • Document 401, 403, and WWW-Authenticate behavior.
  • Configure Spring Security or Express middleware independently.
  • Compare the contract to route behavior in CI.
  • Keep discovery and token URLs environment-correct.
  • Link complex private signing protocols to complete documentation.

Frequently asked questions

Does securitySchemes protect the endpoint?

No. It describes available mechanisms. A security requirement applies the description, while runtime security code performs enforcement.

What is the difference between bearer and API key schemes?

Bearer uses the HTTP Authorization scheme. API key names a header, query parameter, or cookie carrying a key. Their issuance and validation policies remain application-specific.

How do I make one operation public?

Set security: [] on that operation and ensure runtime configuration also permits it. Review public overrides carefully.

How do I require bearer token and API key together?

Put both names in one Security Requirement Object. Two separate objects mean either one is sufficient.

Should OpenID Connect use oauth2 or openIdConnect?

Use openIdConnect when clients should use the provider’s discovery document. Use oauth2 when explicitly describing OAuth flows and endpoints. Runtime identity validation remains required.

Can one API use several schemes?

Yes. Define each reusable scheme and express whether callers may choose one or must satisfy several. Keep that OR/AND relationship identical in gateways, application middleware, tests, and generated client guidance.

Sources

Knowledge check

Check your understanding

Answer both questions correctly to mark this lesson as mastered. You can retry without penalty.

1. Which description best matches OpenAPI Security Schemes Explained?

2. Which statement matches the lesson's quick answer?