This was generated by AI during triage.
Agent Brief
Category: bug
Summary: Replace the bare Unauthorized response in the HTTP and gRPC auth paths with a two-tier 401 that distinguishes "no/invalid Bearer scheme" from "unknown/inactive key", so users who paste a raw cav7_… key without Bearer get a self-explanatory error instead of a generic 401.
Current behavior:
Both surfaces use the shared ParseBearerToken helper in the datastores package, which returns an empty string for any Bearer-scheme problem (no Bearer prefix, empty token after the prefix, oversized token). The HTTP gateway's authMiddleware collapses both the empty-token case and the ValidateApiKey failure case into the same http.Error(w, "Unauthorized", 401). The gRPC unary auth interceptor collapses them similarly. There is already a confirmed support case where a user with a valid, correctly-scoped key spent significant time debugging across Swagger UI, Bruno, and a Go client before realizing the missing Bearer prefix was the problem — because Swagger UI's apiKey security scheme does not auto-prepend it.
Desired behavior:
Differentiate the two 401 branches on both surfaces:
- Bearer-scheme problem (no/empty/oversized token after parsing): return 401 with a body that names the expected format, e.g.
Unauthorized: expected 'Authorization: Bearer <key>' header.
- Key-validation failure (token present but
ValidateApiKey returns nil/err): keep the generic Unauthorized — don't leak whether the key exists, is expired, has zero scopes, etc.
Keep the HTTP and gRPC error wording aligned in spirit so behavior is consistent across surfaces (gRPC will use codes.Unauthenticated rather than HTTP status, but the two-tier message distinction should mirror).
Key interfaces:
- HTTP
authMiddleware in the gateway package — both 401 branches currently emit the bare "Unauthorized" string.
- gRPC unary auth interceptor — same two-branch shape, also collapses today.
ParseBearerToken(raw, maxLen) string in the datastores package — its empty-string return already encodes the scheme-problem case; do not change the helper. The fix is in how callers react to its empty return.
Acceptance criteria:
Out of scope:
Agent Brief
Category: bug
Summary: Replace the bare
Unauthorizedresponse in the HTTP and gRPC auth paths with a two-tier 401 that distinguishes "no/invalid Bearer scheme" from "unknown/inactive key", so users who paste a rawcav7_…key withoutBearerget a self-explanatory error instead of a generic 401.Current behavior:
Both surfaces use the shared
ParseBearerTokenhelper in thedatastorespackage, which returns an empty string for any Bearer-scheme problem (noBearerprefix, empty token after the prefix, oversized token). The HTTP gateway'sauthMiddlewarecollapses both the empty-token case and theValidateApiKeyfailure case into the samehttp.Error(w, "Unauthorized", 401). The gRPC unary auth interceptor collapses them similarly. There is already a confirmed support case where a user with a valid, correctly-scoped key spent significant time debugging across Swagger UI, Bruno, and a Go client before realizing the missingBearerprefix was the problem — because Swagger UI'sapiKeysecurity scheme does not auto-prepend it.Desired behavior:
Differentiate the two 401 branches on both surfaces:
Unauthorized: expected 'Authorization: Bearer <key>' header.ValidateApiKeyreturns nil/err): keep the genericUnauthorized— don't leak whether the key exists, is expired, has zero scopes, etc.Keep the HTTP and gRPC error wording aligned in spirit so behavior is consistent across surfaces (gRPC will use
codes.Unauthenticatedrather than HTTP status, but the two-tier message distinction should mirror).Key interfaces:
authMiddlewarein the gateway package — both 401 branches currently emit the bare"Unauthorized"string.ParseBearerToken(raw, maxLen) stringin thedatastorespackage — its empty-string return already encodes the scheme-problem case; do not change the helper. The fix is in how callers react to its empty return.Acceptance criteria:
Authorizationheader → 401 whose body names the expectedBearer <key>format.Authorization: <raw-key>(noBearerprefix) → 401 whose body names the expected format.Authorization: Bearer <bad-key>→ 401 with the genericUnauthorizedbody, no leak about whether the key existed.codes.Unauthenticatedwith the same two-tier message distinction.datastores/auth_test.goandservers/grpc/auth_test.gostill pass.Out of scope:
type: apiKey→ bearer http). Tracked separately in Migrate OpenAPI spec from Swagger 2.0 to OpenAPI 3 — unlocks bearer HTTP scheme in Swagger UI #91 — needs OpenAPI 3 migration first.ParseBearerTokenitself; its return-value contract is fine.last_used_datebump behavior.maxTokenLencap.