Skip to content

feat(client)!: expose Docker Hub auth as HubAuth sub-client - #618

Draft
Benehiko wants to merge 6 commits into
mainfrom
feat/client-dockerhub-helpers
Draft

feat(client)!: expose Docker Hub auth as HubAuth sub-client#618
Benehiko wants to merge 6 commits into
mainfrom
feat/client-dockerhub-helpers

Conversation

@Benehiko

@Benehiko Benehiko commented Aug 18, 2026

Copy link
Copy Markdown
Member

What

Adds Docker Hub authentication to the client as a typed sub-client, kubernetes-clientset style: client.Client gains a HubAuth() accessor returning a dockerhub.ClientAuth, so consumers no longer need to know the realm layout or the JSON payload format.

c, _ := client.New()
hub := c.HubAuth()

// default signed-in account (resolved via docker/auth/metadata/hub/default)
session, err := hub.GetDefaultSession(ctx)

// or a specific account
session, err = hub.GetSession(ctx, "myuser")

// all signed-in accounts
profiles, err := hub.ListProfiles(ctx)

// staging realms, per accessor
session, err = c.HubAuth(dockerhub.Staging()).GetDefaultSession(ctx)

session.AccessToken       // the raw JWT access token
session.Claims.Username   // typed, decoded token claims
session.Claims.ExpiresAt

Callers holding a bare secrets.Resolver can construct the accessor directly with dockerhub.New(engine).

API surface

  • ClientAuth — the sub-client interface: ListProfiles, GetDefaultProfile, GetDefaultSession, GetSession(username); sentinel errors ErrNoSession and ErrNoDefaultProfile (the latter wraps the former, so errors.Is(err, ErrNoSession) covers both "no default set" and "credential missing")
  • UserSession{AccessToken, Claims} — decoded form of a docker/auth/hub/** envelope payload
  • Claims — full Hub token claims (registered JWT claims + scope, app_name, uuid, source, session_id, client_id, client_name, email, username). Parsing is dependency-free: NumericDate accepts integer and fractional epochs, Audience accepts string and array forms, per RFC 7519
  • Profile — account profile metadata (docker/auth/metadata/hub/**): user_id, username, email, sign-in date, original sign-in app
  • ParseUserSession / ParseProfile — envelope-level decoders for consumers holding raw envelopes

The dockerhub package now imports x/secrets directly (types are aliases of the client package's), which lets the client package import dockerhub without a cycle.

Security

The lookup paths validate stored and caller-supplied identifiers before touching the engine:

  • profile.UserID read from the default-profile metadata is parsed as a strict ID (wildcards rejected) and must lie inside the configured accounts realm. A tampered or corrupt metadata payload can no longer fan out over every readable secret ("user_id": "docker/**") or point across realms/environments (a production token returned as a staging session, or a non-Hub credential returned as a session).
  • Usernames containing / are rejected instead of silently addressing a nested key under the realm.
  • NumericDate rejects out-of-range epochs instead of relying on implementation-defined float-to-int conversion, treats JSON null as a no-op, parses plain decimals exactly from the string (round-tripping through float64 loses sub-second precision at epoch magnitudes), and preserves fractional seconds when marshalling, including correct sign handling for pre-epoch dates.

Design notes

  • Access token only by design — the payload served under the hub realm never carries a refresh token; rotation is the engine's job and clients re-fetch near expiry.
  • ListProfiles sweeps the metadata realm, skips the default pointer entry (its payload duplicates the default account's own entry), and dedupes by user ID. The realm doc comments in client/realms and x/realms now describe these semantics.
  • Envelope decoding is lenient: undecodable envelopes are skipped (first successful parse wins), empty result sets map to the sentinel errors.
  • No new module dependencies; no vendor changes.

Testing

  • Unit tests pin the served wire format with raw JSON fixtures (integer exp, array and string aud, fractional epochs, claims omitted, staging realms, error mapping) and cover the new rejection paths: wildcard and cross-realm user_id, multi-component usernames, out-of-range epochs, and the ListProfiles default-skip and dedupe behavior.
  • Tests use x/testhelper.MockResolver, so lookups exercise real pattern matching instead of exact-string comparison.
  • An earlier, function-shaped revision of these lookups was verified live against the engine embedded in Docker Desktop (default-profile resolution, per-user fetch, not-found mapping); the sub-client reshape covers the same realm queries. Current Desktop builds serve the claims object with empty values — a Desktop-side fix is in flight; the parser handles both shapes, and envelope.ExpiresAt remains the reliable expiry source meanwhile.

🤖 Generated with Claude Code

Benehiko and others added 4 commits August 18, 2026 11:50
Add a client/dockerhub package that fetches Docker Hub access tokens from
the secrets engine and decodes them into typed sessions, so consumers no
longer need to know the realm layout or the JSON payload format.

The package resolves the default signed-in account through the profile
metadata realm (docker/auth/metadata/hub/default) or fetches a specific
username directly under docker/auth/hub/, and parses the served payload
into a UserSession with typed JWT claims. Claim parsing is dependency-free:
numeric dates accept integer and fractional epochs and the audience accepts
both string and array forms, per RFC 7519.

Envelope decoding is deliberately lenient: undecodable envelopes are
skipped, empty result sets map to ErrNoSession/ErrNoDefaultProfile, and
usernames are parsed as strict IDs so wildcard patterns cannot be injected.

Verified live against the engine embedded in Docker Desktop: default
profile resolution, per-user fetch, not-found mapping, and profile
metadata parsing all round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the dockerhub.Client wrapper type and its constructor. Consumers pass
the standard secrets-engine client to package functions instead of
instantiating a second client object; staging becomes a per-call option.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shorten the package, type, and function doc comments to be direct while
keeping the realm layout and error semantics. Drop redundant comments
from the tests, which describe themselves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…m-checked lookups

Reshape client/dockerhub around a typed sub-client, kubernetes-clientset
style: client.Client gains HubAuth(opts ...dockerhub.Option) returning a
dockerhub.ClientAuth with ListProfiles, GetDefaultProfile,
GetDefaultSession and GetSession. The package-level helpers
GetDefaultProfileAccessToken/GetUserAccessToken and the SecretsGetter
interface are removed. dockerhub now imports x/secrets directly so the
client package can import it without a cycle.

Security hardening in the lookup paths:
- profile.UserID from stored metadata is parsed as an ID (wildcards
  rejected) and must lie inside the configured accounts realm, so a
  tampered default-profile payload can no longer fan out over every
  readable secret or point across realms/environments.
- usernames containing '/' are rejected instead of silently addressing
  a nested key.

NumericDate fixes: plain decimals parse exactly from the string (going
through float64 loses sub-second precision at epoch magnitudes),
out-of-range epochs error instead of implementation-defined conversion,
JSON null is a no-op, and marshalling preserves fractional seconds with
correct pre-epoch sign handling.

ListProfiles sweeps the metadata realm, skips the default pointer entry
and dedupes by user ID; the realm doc comments in client/realms and
x/realms now describe the per-account entries plus the default pointer.
Tests move onto x/testhelper.MockResolver for real pattern matching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Benehiko Benehiko changed the title feat(client): add Docker Hub access token helpers feat(client)!: expose Docker Hub auth as HubAuth sub-client Aug 20, 2026
Benehiko and others added 2 commits August 20, 2026 15:11
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Callers that only care about having a usable session can check
errors.Is(err, ErrNoSession) alone; callers that need to distinguish a
missing default pointer from a missing credential can still check
ErrNoDefaultProfile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant