← back to Cli Printing Press
fix(cli): infer bearer auth from inline params (#634)
b4a95cb7d9c803354741dc63ffcc051e4b82d2aa · 2026-05-05 13:43:41 -0700 · Trevin Chow
* fix(cli): infer bearer auth from inline params
* docs(cli): document inline bearer auth inference
Files touched
A docs/solutions/logic-errors/inline-authorization-param-bearer-inference-2026-05-05.mdM internal/openapi/parser.goM internal/openapi/parser_test.goM testdata/openapi/auth-header-param.yaml
Diff
commit b4a95cb7d9c803354741dc63ffcc051e4b82d2aa
Author: Trevin Chow <trevin@trevinchow.com>
Date: Tue May 5 13:43:41 2026 -0700
fix(cli): infer bearer auth from inline params (#634)
* fix(cli): infer bearer auth from inline params
* docs(cli): document inline bearer auth inference
---
...horization-param-bearer-inference-2026-05-05.md | 113 +++++++++++++++++++++
internal/openapi/parser.go | 61 +++++++----
internal/openapi/parser_test.go | 101 +++++++++++++++++-
testdata/openapi/auth-header-param.yaml | 12 ++-
4 files changed, 261 insertions(+), 26 deletions(-)
diff --git a/docs/solutions/logic-errors/inline-authorization-param-bearer-inference-2026-05-05.md b/docs/solutions/logic-errors/inline-authorization-param-bearer-inference-2026-05-05.md
new file mode 100644
index 00000000..afe6ab72
--- /dev/null
+++ b/docs/solutions/logic-errors/inline-authorization-param-bearer-inference-2026-05-05.md
@@ -0,0 +1,113 @@
+---
+title: "Inline Authorization params: conservative bearer inference"
+date: 2026-05-05
+category: logic-errors
+module: internal/openapi
+problem_type: logic_error
+component: authentication
+symptoms:
+ - "OpenAPI specs without top-level securitySchemes or security failed to infer bearer_token auth from required operation-level Authorization header params"
+ - "Cal.com-style specs exposed Authorization as inline operation parameters instead of reusable OpenAPI security declarations"
+ - "Generated CLIs could omit bearer auth wiring unless manual Phase 2 enrichment patched auth metadata"
+root_cause: logic_error
+resolution_type: code_fix
+severity: medium
+related_components:
+ - testing_framework
+tags:
+ - openapi-parser
+ - bearer-auth
+ - authorization-header
+ - inline-parameters
+ - security-inference
+---
+
+# Inline Authorization params: conservative bearer inference
+
+## Problem
+
+Some OpenAPI specs declare authentication as a required operation-level `Authorization` header parameter instead of using `components.securitySchemes` or top-level `security`. The parser treated those specs as unauthenticated, so generated CLIs could miss bearer auth setup unless the skill flow manually enriched the spec before generation.
+
+## Symptoms
+
+- Cal.com-style specs had no top-level OpenAPI security declaration, but most operations required an `Authorization` header.
+- Parsed specs could end up with `auth.type: none` even though the API required bearer credentials.
+- Manual auth enrichment became the fallback for a spec shape the parser could infer safely.
+
+## What Didn't Work
+
+- **Description-only auth inference.** Specs may have sparse descriptions, or descriptions may mention bearer tokens in examples, warnings, migration notes, or unrelated prose.
+- **Treating any `Authorization` header as bearer.** Adjacent auth-shape sessions showed that auth-looking fields can be non-bearer, query-key, composed, or session-handshake auth. A permissive "Authorization means bearer" rule would repeat that class of bug (session history).
+- **Running inline inference when explicit security exists.** `securitySchemes` and top-level `security` are the canonical OpenAPI declarations. A fallback must not compete with them.
+
+## Solution
+
+Add a fourth-tier fallback in `mapAuth`: after explicit security schemes, query-param auth, and description auth all fail, infer bearer auth from inline operation parameters only when the evidence is broad and scheme-specific.
+
+The fallback has 4 guards:
+
+1. The spec has no `components.securitySchemes`.
+2. The spec has no top-level `security` declaration.
+3. At least 80% of operations have a required `Authorization` header parameter.
+4. The header parameter or its schema description contains a non-negated Bearer mention.
+
+```go
+func inferOperationLevelBearer(doc *openapi3.T, name string, fallback spec.AuthConfig) spec.AuthConfig {
+ if doc == nil || doc.Paths == nil {
+ return fallback
+ }
+ if hasTopLevelSecurityDeclaration(doc) {
+ return fallback
+ }
+
+ authParamCount := 0
+ hasBearerSignal := false
+ totalOps := 0
+
+ // Count operations with a required Authorization header and require a
+ // non-negated Bearer signal before synthesizing bearer_token auth.
+}
+```
+
+Use the parser's existing helpers instead of creating nearby duplicate semantics:
+
+```go
+func requiredAuthorizationParam(pathItem *openapi3.PathItem, op *openapi3.Operation) (*openapi3.Parameter, bool) {
+ for _, p := range mergeParameters(pathItem, op) {
+ if p.In == openapi3.ParameterInHeader && strings.EqualFold(p.Name, "Authorization") && p.Required {
+ return p, true
+ }
+ }
+ return nil, false
+}
+```
+
+`mergeParameters` preserves the parser's existing path-level and operation-level parameter precedence. `findUnnegated` avoids false positives from descriptions such as `Do not use Bearer prefix.`
+
+## Why This Works
+
+The fix recognizes a real generated-spec pattern without turning auth inference into guesswork. Broad required-header coverage is structural evidence that the API expects auth on most operations. A non-negated Bearer mention on the parameter or schema narrows that auth shape to bearer-token auth.
+
+The explicit-security guard keeps the fallback behind authoritative spec declarations. Reusing `mergeParameters` keeps parameter resolution consistent with the rest of `internal/openapi/parser.go`, so path-level and operation-level params do not drift between inference paths.
+
+## Prevention
+
+- Order auth inference from most authoritative to least authoritative: explicit security schemes, query-param auth, description auth, then inline operation-level bearer inference.
+- Require both structural evidence and textual scheme evidence before inferring bearer auth from header params.
+- Use negation-aware matching for auth-scheme mentions.
+- Reuse parser helpers such as `mergeParameters` when adding inference over OpenAPI parameters.
+- Preserve regression coverage for:
+ - positive Cal.com-style inline `Authorization` params
+ - exact 80% coverage threshold
+ - below-threshold coverage
+ - missing Bearer signal
+ - negated Bearer signal
+ - optional `Authorization` header ignored
+ - explicit `securitySchemes` or `security` winning over fallback inference
+
+## Related Issues
+
+- [mvanhorn/cli-printing-press#600](https://github.com/mvanhorn/cli-printing-press/issues/600) -- direct issue for operation-level Authorization bearer inference
+- [mvanhorn/cli-printing-press#634](https://github.com/mvanhorn/cli-printing-press/pull/634) -- implementation PR
+- [mvanhorn/cli-printing-press#597](https://github.com/mvanhorn/cli-printing-press/issues/597) -- parent Cal.com retro
+- [mvanhorn/cli-printing-press#517](https://github.com/mvanhorn/cli-printing-press/issues/517) -- related Phase 2 auth enrichment umbrella
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index b416e158..ae2a3ff5 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -368,7 +368,7 @@ func mapAuth(doc *openapi3.T, name string) spec.AuthConfig {
result = inferDescriptionAuth(doc, name, result)
}
if result.Type == "none" {
- result = inferAuthHeaderParam(doc, name, result)
+ result = inferOperationLevelBearer(doc, name, result)
}
return result
}
@@ -969,17 +969,20 @@ func inferDescriptionAuth(doc *openapi3.T, name string, fallback spec.AuthConfig
return fallback
}
-// inferAuthHeaderParam scans all operations for required Authorization header
-// parameters. This is the fourth-tier auth fallback — it fires only when
-// securitySchemes, query-param inference, and description inference all fail.
-// APIs like Cal.com declare auth via individual header parameters instead of
-// securitySchemes or description text.
-func inferAuthHeaderParam(doc *openapi3.T, name string, fallback spec.AuthConfig) spec.AuthConfig {
+// inferOperationLevelBearer scans all operations for required Authorization
+// header parameters that identify themselves as Bearer tokens. This is the
+// fourth-tier auth fallback — it fires only when securitySchemes, query-param
+// inference, and description inference all fail.
+func inferOperationLevelBearer(doc *openapi3.T, name string, fallback spec.AuthConfig) spec.AuthConfig {
if doc == nil || doc.Paths == nil {
return fallback
}
+ if hasTopLevelSecurityDeclaration(doc) {
+ return fallback
+ }
authParamCount := 0
+ hasBearerSignal := false
totalOps := 0
for _, pathKey := range doc.Paths.InMatchingOrder() {
@@ -992,24 +995,16 @@ func inferAuthHeaderParam(doc *openapi3.T, name string, fallback spec.AuthConfig
continue
}
totalOps++
- for _, params := range []openapi3.Parameters{pathItem.Parameters, op.Parameters} {
- for _, pRef := range params {
- if pRef == nil || pRef.Value == nil {
- continue
- }
- p := pRef.Value
- if p.In == openapi3.ParameterInHeader &&
- strings.EqualFold(p.Name, "Authorization") &&
- p.Required {
- authParamCount++
- break // count once per operation
- }
+ if authParam, ok := requiredAuthorizationParam(pathItem, op); ok {
+ authParamCount++
+ if authorizationParamMentionsBearer(authParam) {
+ hasBearerSignal = true
}
}
}
}
- if totalOps == 0 || float64(authParamCount)/float64(totalOps) <= 0.3 {
+ if totalOps == 0 || !hasBearerSignal || float64(authParamCount)/float64(totalOps) < 0.8 {
return fallback
}
@@ -1023,6 +1018,32 @@ func inferAuthHeaderParam(doc *openapi3.T, name string, fallback spec.AuthConfig
}
}
+func hasTopLevelSecurityDeclaration(doc *openapi3.T) bool {
+ return (doc.Components != nil && len(doc.Components.SecuritySchemes) > 0) || doc.Security != nil
+}
+
+func requiredAuthorizationParam(pathItem *openapi3.PathItem, op *openapi3.Operation) (*openapi3.Parameter, bool) {
+ for _, p := range mergeParameters(pathItem, op) {
+ if p.In == openapi3.ParameterInHeader && strings.EqualFold(p.Name, "Authorization") && p.Required {
+ return p, true
+ }
+ }
+ return nil, false
+}
+
+func authorizationParamMentionsBearer(p *openapi3.Parameter) bool {
+ if p == nil {
+ return false
+ }
+ if findUnnegated(strings.ToLower(p.Description), "bearer") {
+ return true
+ }
+ if p.Schema != nil && p.Schema.Value != nil {
+ return findUnnegated(strings.ToLower(p.Schema.Value.Description), "bearer")
+ }
+ return false
+}
+
// commonCustomHeaders are header names that APIs use instead of Authorization.
// Checked case-insensitively against the description text.
var commonCustomHeaders = []string{
diff --git a/internal/openapi/parser_test.go b/internal/openapi/parser_test.go
index 6b5d4c31..6fa45ce3 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -1269,10 +1269,10 @@ paths:
assert.Equal(t, "Optional FlightAware AeroAPI credential for enriched flight data.", parsed.Auth.Description)
}
-func TestInferAuthHeaderParam(t *testing.T) {
+func TestInferOperationLevelBearer(t *testing.T) {
t.Parallel()
- t.Run("detects auth from required Authorization header params", func(t *testing.T) {
+ t.Run("detects bearer auth from required Authorization header params", func(t *testing.T) {
data, err := os.ReadFile(filepath.Join("..", "..", "testdata", "openapi", "auth-header-param.yaml"))
require.NoError(t, err)
@@ -1287,7 +1287,7 @@ func TestInferAuthHeaderParam(t *testing.T) {
})
t.Run("does not trigger when Authorization params below threshold", func(t *testing.T) {
- // 1 out of 5 operations = 20% < 30% threshold
+ // 1 out of 5 operations = 20% < 80% threshold
doc := &openapi3.T{
Info: &openapi3.Info{Title: "test", Description: "no auth keywords"},
Paths: &openapi3.Paths{},
@@ -1299,7 +1299,7 @@ func TestInferAuthHeaderParam(t *testing.T) {
if i == 0 { // only first has Authorization param
pathItem.Get.Parameters = openapi3.Parameters{
&openapi3.ParameterRef{Value: &openapi3.Parameter{
- Name: "Authorization", In: "header", Required: true,
+ Name: "Authorization", In: "header", Required: true, Description: "Bearer token credential",
}},
}
}
@@ -1309,6 +1309,74 @@ func TestInferAuthHeaderParam(t *testing.T) {
assert.Equal(t, "none", result.Type)
})
+ t.Run("detects auth at exact eighty percent threshold", func(t *testing.T) {
+ doc := &openapi3.T{
+ Info: &openapi3.Info{Title: "test", Description: "no auth keywords"},
+ Paths: &openapi3.Paths{},
+ }
+ for i, path := range []string{"/a", "/b", "/c", "/d", "/e"} {
+ pathItem := &openapi3.PathItem{
+ Get: &openapi3.Operation{Responses: openapi3.NewResponses()},
+ }
+ if i < 4 {
+ pathItem.Get.Parameters = openapi3.Parameters{
+ &openapi3.ParameterRef{Value: &openapi3.Parameter{
+ Name: "Authorization", In: "header", Required: true, Description: "Bearer token credential",
+ }},
+ }
+ }
+ doc.Paths.Set(path, pathItem)
+ }
+ result := mapAuth(doc, "test-api")
+ assert.Equal(t, "bearer_token", result.Type)
+ assert.Equal(t, []string{"TEST_API_TOKEN"}, result.EnvVars)
+ assert.True(t, result.Inferred)
+ })
+
+ t.Run("does not infer bearer without bearer signal", func(t *testing.T) {
+ doc := &openapi3.T{
+ Info: &openapi3.Info{Title: "test", Description: "no auth keywords"},
+ Paths: &openapi3.Paths{},
+ }
+ for _, path := range []string{"/a", "/b", "/c", "/d", "/e"} {
+ pathItem := &openapi3.PathItem{
+ Get: &openapi3.Operation{
+ Responses: openapi3.NewResponses(),
+ Parameters: openapi3.Parameters{
+ &openapi3.ParameterRef{Value: &openapi3.Parameter{
+ Name: "Authorization", In: "header", Required: true,
+ }},
+ },
+ },
+ }
+ doc.Paths.Set(path, pathItem)
+ }
+ result := mapAuth(doc, "test-api")
+ assert.Equal(t, "none", result.Type)
+ })
+
+ t.Run("does not infer bearer from negated bearer signal", func(t *testing.T) {
+ doc := &openapi3.T{
+ Info: &openapi3.Info{Title: "test", Description: "no auth keywords"},
+ Paths: &openapi3.Paths{},
+ }
+ for _, path := range []string{"/a", "/b", "/c", "/d", "/e"} {
+ pathItem := &openapi3.PathItem{
+ Get: &openapi3.Operation{
+ Responses: openapi3.NewResponses(),
+ Parameters: openapi3.Parameters{
+ &openapi3.ParameterRef{Value: &openapi3.Parameter{
+ Name: "Authorization", In: "header", Required: true, Description: "Do not use Bearer prefix.",
+ }},
+ },
+ },
+ }
+ doc.Paths.Set(path, pathItem)
+ }
+ result := mapAuth(doc, "test-api")
+ assert.Equal(t, "none", result.Type)
+ })
+
t.Run("optional Authorization param not counted", func(t *testing.T) {
doc := &openapi3.T{
Info: &openapi3.Info{Title: "test", Description: "no auth keywords"},
@@ -1320,7 +1388,30 @@ func TestInferAuthHeaderParam(t *testing.T) {
Responses: openapi3.NewResponses(),
Parameters: openapi3.Parameters{
&openapi3.ParameterRef{Value: &openapi3.Parameter{
- Name: "Authorization", In: "header", Required: false,
+ Name: "Authorization", In: "header", Required: false, Description: "Bearer token credential",
+ }},
+ },
+ },
+ }
+ doc.Paths.Set(path, pathItem)
+ }
+ result := mapAuth(doc, "test-api")
+ assert.Equal(t, "none", result.Type)
+ })
+
+ t.Run("top-level security declaration disables inline inference", func(t *testing.T) {
+ doc := &openapi3.T{
+ Info: &openapi3.Info{Title: "test", Description: "no auth keywords"},
+ Paths: &openapi3.Paths{},
+ Security: openapi3.SecurityRequirements{},
+ }
+ for _, path := range []string{"/a", "/b", "/c", "/d", "/e"} {
+ pathItem := &openapi3.PathItem{
+ Get: &openapi3.Operation{
+ Responses: openapi3.NewResponses(),
+ Parameters: openapi3.Parameters{
+ &openapi3.ParameterRef{Value: &openapi3.Parameter{
+ Name: "Authorization", In: "header", Required: true, Description: "Bearer token credential",
}},
},
},
diff --git a/testdata/openapi/auth-header-param.yaml b/testdata/openapi/auth-header-param.yaml
index a9512b54..51137bf4 100644
--- a/testdata/openapi/auth-header-param.yaml
+++ b/testdata/openapi/auth-header-param.yaml
@@ -14,6 +14,7 @@ paths:
- name: Authorization
in: header
required: true
+ description: "Bearer token credential"
schema:
type: string
responses:
@@ -32,6 +33,7 @@ paths:
- name: Authorization
in: header
required: true
+ description: "Bearer token credential"
schema:
type: string
responses:
@@ -45,6 +47,7 @@ paths:
- name: Authorization
in: header
required: true
+ description: "Bearer token credential"
schema:
type: string
responses:
@@ -53,7 +56,14 @@ paths:
/v2/health:
get:
operationId: healthCheck
- summary: Health check (no auth)
+ summary: Health check
+ parameters:
+ - name: Authorization
+ in: header
+ required: true
+ description: "Bearer token credential"
+ schema:
+ type: string
responses:
"200":
description: OK
← 6b5945fd fix(cli): preserve canonical auth env hints (#633)
·
back to Cli Printing Press
·
fix(cli): require explicit retry no-ops (#635) cb3ef873 →