Skip to content

auth: 401 message should hint at expected "Bearer <key>" scheme #99

@SyniRon

Description

@SyniRon

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:

  • HTTP request with no Authorization header → 401 whose body names the expected Bearer <key> format.
  • HTTP request with Authorization: <raw-key> (no Bearer prefix) → 401 whose body names the expected format.
  • HTTP request with Authorization: Bearer <bad-key> → 401 with the generic Unauthorized body, no leak about whether the key existed.
  • gRPC unary auth returns analogous codes.Unauthenticated with the same two-tier message distinction.
  • Existing tests in datastores/auth_test.go and servers/grpc/auth_test.go still pass.
  • New test coverage exercises both 401 branches on both the HTTP and gRPC surfaces.

Out of scope:

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready-for-agentFully specified, ready for an AFK agent to implement

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions