chore(auth): /v2/auth/token handler

Summary

Step 4 of the S08 authentication stub plan: the /v2/auth/token handler.

What it does

  • GET /v2/auth/token, Basic credential matches → 200. JSON envelope per spec: token, access_token, expires_in: 86400, issued_at (RFC3339). Username on the Basic credential is ignored (spec-mandated).
  • Missing / empty / malformed / mismatched credential → 401. WWW-Authenticate: Basic realm=\"artifact-registry\", empty body. Spec collapses the four cases into one response (no information leak between them).
  • Non-GET → 405. Allow: GET header + S01 envelope (code: \"method_not_allowed\") via internal/transport.WriteError. Plan-territory decision, not a spec restatement — the spec documents GET only; the plan picks 405 by analogy with S01's wrong-method response.
  • Validator-surfaced context.Canceled / context.DeadlineExceeded → skip response. Mirrors the dispatch middleware's cancellation handling — writing a 401 to a closed connection is a stale write that pollutes credential-failure metrics with transport cancellations.

Code reuse

The handler calls the package-private extractCredential(r, SchemeBasic) shipped in Step 3 (alongside the dispatch middleware). Same parsing semantics, same secret.Secret boundary, no duplicated Basic-header parser. No new exported surface beyond auth.TokenHandler.

Spec coverage

Spec: docs/specs/S08-authentication.md

Acceptance criteria

# Criterion Tests
AC-1 Dispatch table coverage Owned by Step 3 (internal/auth/middleware_test.go). Not in scope for Step 4.
AC-2 /v2/auth/token handler — 200 JSON shape on match; 401 + Basic realm=\"artifact-registry\" on missing/empty/malformed/mismatched TestTokenHandler_PositiveGETReturnsBootstrapToken, TestTokenHandler_PositiveIgnoresUsername, TestTokenHandler_Negative401
AC-3 Constant-time comparison in the credential check Owned by Step 5 (internal/auth/bootstrap/bootstrap_test.go, timing-variance test). Not in scope for Step 4 (the authtest.Fake uses plain ==).
AC-4 Build-tag fence — default build cannot link the bootstrap validator Owned by Step 5 (scripts/check-no-devstubs.sh, lint:no-devstubs-in-prod CI job). Not in scope for Step 4.
AC-5 Configuration loading — env unset / file missing / file empty / both set / neither set Owned by Step 1 (internal/secret/resolver_test.go). Not in scope for Step 4.

Error cases

# Condition Tests
E-1 Missing/empty/malformed/mismatched credential on a non-OCI prefix: 401 + challenge + empty body TestTokenHandler_Negative401 (5 subtests: missing, empty, malformed-scheme, malformed-base64, mismatched)
E-2 Unknown path prefix: 404 + S01 envelope Owned by Step 3 (TestMiddleware_FallbackNotFound). Not in scope for Step 4 — the handler is only reachable via dispatch.

Plan-territory additions (not in spec)

# Behavior Tests
P-1 Non-GET on /v2/auth/token → 405 + Allow: GET + S01 envelope TestTokenHandler_MethodNotAllowed
P-2 Validator-surfaced ctx cancellation skips response (mirrors middleware) TestTokenHandler_CancellationSkipsResponse

Security considerations

# Concern Tests
S-1 Bootstrap-token validator gated behind development_stubs build tag (the spec's entire security argument) Owned by Step 5 (build-tag fence test + CI). Gating is enforced upstream of TokenHandler.

Test plan

  • CI pipeline green
  • go test ./internal/auth/... passes locally (12 new tests in TestTokenHandler_*)
  • Full suite green locally (456 tests across 15 packages at branch HEAD)
  • Reviewer confirms 405-on-non-GET decision matches the rationale in the handler godoc (analogy with S01's wrong-method response)
  • Reviewer confirms cancellation skip-response branch matches middleware semantics
Edited by Moaz Khalifa

Merge request reports

Loading
Loading