📖 6 min read (~ 1300 words).

Security

OpenAPI 2.0 splits authentication into two parts: security definitions name the schemes (an API key, OAuth2, HTTP Basic), and security requirements reference those schemes — document-wide and/or per operation. The panes below are backed by the test-covered docs/examples/concepts/security package.

Declare the schemes

A SecurityDefinitions: block in swagger:meta declares every scheme once; a Security: block sets the document-wide default requirement that applies to operations that do not state their own.

Package doc comment
// Package security Reports API.
//
// The swagger:meta block declares the security schemes once and sets the
// document-wide default requirement.
//
//	Version: 1.0.0
//
//	SecurityDefinitions:
//	  api_key:
//	    type: apiKey
//	    in: header
//	    name: X-API-Key
//	  oauth2:
//	    type: oauth2
//	    flow: accessCode
//	    authorizationUrl: https://example.com/auth
//	    tokenUrl: https://example.com/token
//	    scopes:
//	      read: read reports
//	      write: write reports
//
//	Security:
//	  - api_key: []
//
// swagger:meta
package security

Full source: docs/examples/concepts/security/doc.go

securityDefinitions + security
{
  "security": [
    {
      "api_key": []
    }
  ],
  "securityDefinitions": {
    "api_key": {
      "type": "apiKey",
      "name": "X-API-Key",
      "in": "header"
    },
    "oauth2": {
      "type": "oauth2",
      "flow": "accessCode",
      "authorizationUrl": "https://example.com/auth",
      "tokenUrl": "https://example.com/token",
      "scopes": {
        "read": "read reports",
        "write": "write reports"
      }
    }
  }
}

Full source: docs/examples/concepts/security/testdata/schemes.json

The scheme type drives the rest: apiKey needs in + name, oauth2 needs a flow (and the URLs/scopes it implies), basic needs nothing more. The full scheme surface is in the securityDefinitions reference.

Require a scheme on a route

A route with no Security: keyword inherits the document-wide default (api_key, above). A route that needs something different states its own Security: requirement — here createReport requires oauth2 with the read and write scopes, overriding the default:

swagger:route
// listReports inherits the document-wide default requirement (api_key) — no
// Security: keyword is needed.
//
// swagger:route GET /reports reports listReports
//
// responses:
//   200: description: the reports

// createReport overrides the default with its own Security: requirement —
// oauth2 with the read and write scopes. The Security: block is YAML: a sequence
// of requirement objects, scopes as a flow (or block) list.
//
// swagger:route POST /reports reports createReport
//
// Security:
//   - oauth2: [read, write]
//
// responses:
//   201: description: created

// archiveReport requires BOTH schemes at once — two keys in a single sequence
// item are ANDed into one requirement object (separate items would mean OR).
//
// swagger:route POST /reports/archive reports archiveReport
//
// Security:
//   - api_key: []
//     oauth2: [write]
//
// responses:
//   200: description: archived

// publicReport opts out of the document default entirely — an empty
// `Security: []` emits an explicit empty requirement, marking the operation
// public regardless of the document-wide default.
//
// swagger:route GET /reports/public reports publicReport
//
// Security: []
//
// responses:
//   200: description: the public reports

Full source: docs/examples/concepts/security/routes.go

[
  {
    "oauth2": [
      "read",
      "write"
    ]
  }
]

Full source: docs/examples/concepts/security/testdata/route.json

A Security: block is plain YAML — a sequence of requirement objects. Scopes are a flow list ([read, write]) or a block list; an empty list (api_key: []) is the scheme with no scopes. The combining rule follows OpenAPI 2.0:

  • multiple schemes in one item are ANDed — all are required;
  • separate items are ORed — satisfying any one grants access.

So requiring both an API key and an OAuth2 scope is two keys under a single sequence item:

swagger:route
// listReports inherits the document-wide default requirement (api_key) — no
// Security: keyword is needed.
//
// swagger:route GET /reports reports listReports
//
// responses:
//   200: description: the reports

// createReport overrides the default with its own Security: requirement —
// oauth2 with the read and write scopes. The Security: block is YAML: a sequence
// of requirement objects, scopes as a flow (or block) list.
//
// swagger:route POST /reports reports createReport
//
// Security:
//   - oauth2: [read, write]
//
// responses:
//   201: description: created

// archiveReport requires BOTH schemes at once — two keys in a single sequence
// item are ANDed into one requirement object (separate items would mean OR).
//
// swagger:route POST /reports/archive reports archiveReport
//
// Security:
//   - api_key: []
//     oauth2: [write]
//
// responses:
//   200: description: archived

// publicReport opts out of the document default entirely — an empty
// `Security: []` emits an explicit empty requirement, marking the operation
// public regardless of the document-wide default.
//
// swagger:route GET /reports/public reports publicReport
//
// Security: []
//
// responses:
//   200: description: the public reports

Full source: docs/examples/concepts/security/routes.go

security on archiveReport (AND)
[
  {
    "api_key": [],
    "oauth2": [
      "write"
    ]
  }
]

Full source: docs/examples/concepts/security/testdata/and.json

A route’s requirements replace the document default for that operation. To make one operation public — opting out of the document-wide default — give it an empty Security: []. That emits an explicit empty requirement (distinct from omitting the keyword, which inherits the default):

swagger:route
// listReports inherits the document-wide default requirement (api_key) — no
// Security: keyword is needed.
//
// swagger:route GET /reports reports listReports
//
// responses:
//   200: description: the reports

// createReport overrides the default with its own Security: requirement —
// oauth2 with the read and write scopes. The Security: block is YAML: a sequence
// of requirement objects, scopes as a flow (or block) list.
//
// swagger:route POST /reports reports createReport
//
// Security:
//   - oauth2: [read, write]
//
// responses:
//   201: description: created

// archiveReport requires BOTH schemes at once — two keys in a single sequence
// item are ANDed into one requirement object (separate items would mean OR).
//
// swagger:route POST /reports/archive reports archiveReport
//
// Security:
//   - api_key: []
//     oauth2: [write]
//
// responses:
//   200: description: archived

// publicReport opts out of the document default entirely — an empty
// `Security: []` emits an explicit empty requirement, marking the operation
// public regardless of the document-wide default.
//
// swagger:route GET /reports/public reports publicReport
//
// Security: []
//
// responses:
//   200: description: the public reports

Full source: docs/examples/concepts/security/routes.go

security on publicReport
{
  "security": []
}

Full source: docs/examples/concepts/security/testdata/public.json

The same works from a swagger:operation YAML body — a security: key there sets that operation’s requirement. (The schemes themselves are always global swagger:meta — OpenAPI 2.0 has no per-operation securityDefinitions.)

Keep security out of your code

Authentication is often handled by a layer in front of the app — a gateway or service mesh — and you may not want security details in the annotations at all. In that case, leave the code free of security annotations and overlay the schemes and requirements with Options.InputSpec:

// base carries only the security scheme + default requirement.
var base spec.Swagger
_ = json.Unmarshal(baseSpecJSON, &base)

doc, _ := codescan.Run(&codescan.Options{
    Packages:   []string{"./..."},
    ScanModels: true,
    InputSpec:  &base, // securityDefinitions + security come from here
})

The app package above (concepts/routes) carries no security annotations, yet the merged document is secured — the schemes and the default requirement come entirely from the base:

{
  "security": [
    {
      "api_key": []
    }
  ],
  "securityDefinitions": {
    "api_key": {
      "type": "apiKey",
      "name": "X-API-Key",
      "in": "header"
    }
  }
}

Full source: docs/examples/concepts/security/testdata/overlay.json

See Overlaying a spec for the full InputSpec merge semantics.

What’s next