Summary
Docker Compose now uses Docker Desktop's Docker Hub session (via client/dockerhub) to authenticate pulls. To get a credential it can actually use, Compose had to write about 120 lines on top of the SDK. Most of that code isn't specific to Compose. Any consumer that wants "the signed-in user's Docker Hub credential" (the CLI, buildx, other tools) would have to write the same thing. This issue lists what client/dockerhub (checked at client/v0.1.2 and main) is missing, with links to the Compose code that fills each gap.
Compose implementation: internal/registry/desktop.go
1. GetDefaultSession does not give you a username
A registry credential needs a username. containerd treats a password with no username as a refresh token. UserSession.Claims.Username is often empty, so Compose has to call GetDefaultProfile again:
https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L186-L199
session, err := s.hub.GetDefaultSession(ctx)
// ...
username := session.Claims.Username
if username == "" {
profile, err := s.hub.GetDefaultProfile(ctx) // second round trip
// ...
username = profile.Username
}
if username == "" {
return ..., errNoUsername
}
Internally, GetDefaultSession already loads the default profile to find the account entry, then discards it. So callers pay for two profile lookups.
Ask: return the username, or the whole profile, with the session. For example, add Profile/Username to UserSession, or provide a dedicated call (see the proposal below).
2. No token expiry helper
To know how long a token can be reused, Compose reads Claims.ExpiresAt. If that is missing, it decodes the JWT payload itself and reuses dockerhub.NumericDate to parse exp:
https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L217-L240
func sessionExpiry(session dockerhub.UserSession) (time.Time, bool) {
if exp := session.Claims.ExpiresAt; exp != nil {
return exp.Time, true
}
return jwtExpiry(session.AccessToken) // split, base64-decode, unmarshal {"exp"}
}
The package already owns Claims and NumericDate, and documents that "Claims are zero when the payload carries none". Parsing the token belongs there.
Ask: func (s UserSession) ExpiresAt() (time.Time, bool), and/or fill in Claims from the JWT when the stored payload leaves them out.
3. An expired session is returned as if it were valid
GetDefaultSession returns a stored token even after it has expired (for example, the user signed out or Desktop hasn't refreshed it). Every caller has to check the expiry itself and define its own error:
https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L201-L208
var errSessionExpired = errors.New("the session has expired (not signed in to Docker Desktop?)")
// ...
if expiresAt, ok := sessionExpiry(session); ok {
validUntil = expiresAt.Add(-sessionExpirySkew)
if !now.Before(validUntil) {
return ..., errSessionExpired
}
}
Ask: a dockerhub.ErrSessionExpired that wraps ErrNoSession, returned for expired tokens, ideally with a configurable clock-skew margin.
4. No single way to check "no usable session"
To decide between falling back quietly to other credentials and warning the user, Compose checks three unrelated errors:
https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L242-L252
func isSessionUnavailable(err error) bool {
return errors.Is(err, seclient.ErrSecretsEngineNotAvailable) ||
errors.Is(err, dockerhub.ErrNoSession) ||
errors.Is(err, errSessionExpired)
}
Ask: with (3) in place, document a single check for this case (e.g. errors.Is(err, dockerhub.ErrNoSession) also covering "engine not available"), or provide a helper such as dockerhub.IsUnavailable(err).
5. Connecting to Desktop requires the unstable x module
The client.New doc says "To connect to Docker Desktop, use WithSocketPath(api.DesktopSocketPath())". But api lives in github.com/docker/secrets-engine/x, so a stable-client consumer has to depend on x just to find the socket:
https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L86-L95
c, err := seclient.New(
seclient.WithSocketPath(api.DesktopSocketPath()), // github.com/docker/secrets-engine/x/api
seclient.WithTimeout(desktopLookupTimeout),
)
return c.HubAuth(opts...), nil
Ask: client.WithDesktopSocket() / client.NewDesktop(...), or re-export DesktopSocketPath from client.
6. (Optional) A caching, retrying credential source
Compose also wraps the lookup with a cache: one shared lookup for concurrent pulls, token reuse until it nearly expires, and a backoff after failures so it doesn't hit a hung or absent Desktop on every pull:
https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L132-L169
This is generic too. A wrapper such as dockerhub.NewCachedAuth(auth, opts...) would be useful, but it is the most debatable item if the SDK wants to stay stateless.
Proposal
Items 1–3 together could be one call that returns what a registry client needs:
type Credential struct {
Username string
AccessToken string
ExpiresAt time.Time // zero if unknown
}
// GetDefaultCredential returns the default account's credential. It returns
// ErrNoDefaultProfile / ErrNoSession when nobody is signed in, and
// ErrSessionExpired (wrapping ErrNoSession) when the stored token has expired.
GetDefaultCredential(ctx context.Context) (Credential, error)
With that, plus (4) and (5), the Compose side shrinks to building a types.AuthConfig from Credential. jwtExpiry, sessionExpiry, errNoUsername, errSessionExpired, isSessionUnavailable, the second profile lookup and the dependency on x/api would all go away. The matching Compose tests (TestJWTExpiry, …ExpiryFromTokenWhenClaimsLackIt, …UsernameFromProfile, TestIsSessionUnavailable) would move here.
Happy to send PRs for any of these.
Summary
Docker Compose now uses Docker Desktop's Docker Hub session (via
client/dockerhub) to authenticate pulls. To get a credential it can actually use, Compose had to write about 120 lines on top of the SDK. Most of that code isn't specific to Compose. Any consumer that wants "the signed-in user's Docker Hub credential" (the CLI, buildx, other tools) would have to write the same thing. This issue lists whatclient/dockerhub(checked atclient/v0.1.2andmain) is missing, with links to the Compose code that fills each gap.Compose implementation:
internal/registry/desktop.go1.
GetDefaultSessiondoes not give you a usernameA registry credential needs a username. containerd treats a password with no username as a refresh token.
UserSession.Claims.Usernameis often empty, so Compose has to callGetDefaultProfileagain:https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L186-L199
Internally,
GetDefaultSessionalready loads the default profile to find the account entry, then discards it. So callers pay for two profile lookups.Ask: return the username, or the whole profile, with the session. For example, add
Profile/UsernametoUserSession, or provide a dedicated call (see the proposal below).2. No token expiry helper
To know how long a token can be reused, Compose reads
Claims.ExpiresAt. If that is missing, it decodes the JWT payload itself and reusesdockerhub.NumericDateto parseexp:https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L217-L240
The package already owns
ClaimsandNumericDate, and documents that "Claims are zero when the payload carries none". Parsing the token belongs there.Ask:
func (s UserSession) ExpiresAt() (time.Time, bool), and/or fill inClaimsfrom the JWT when the stored payload leaves them out.3. An expired session is returned as if it were valid
GetDefaultSessionreturns a stored token even after it has expired (for example, the user signed out or Desktop hasn't refreshed it). Every caller has to check the expiry itself and define its own error:https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L201-L208
Ask: a
dockerhub.ErrSessionExpiredthat wrapsErrNoSession, returned for expired tokens, ideally with a configurable clock-skew margin.4. No single way to check "no usable session"
To decide between falling back quietly to other credentials and warning the user, Compose checks three unrelated errors:
https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L242-L252
Ask: with (3) in place, document a single check for this case (e.g.
errors.Is(err, dockerhub.ErrNoSession)also covering "engine not available"), or provide a helper such asdockerhub.IsUnavailable(err).5. Connecting to Desktop requires the unstable
xmoduleThe
client.Newdoc says "To connect to Docker Desktop, useWithSocketPath(api.DesktopSocketPath())". Butapilives ingithub.com/docker/secrets-engine/x, so a stable-client consumer has to depend onxjust to find the socket:https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L86-L95
Ask:
client.WithDesktopSocket()/client.NewDesktop(...), or re-exportDesktopSocketPathfromclient.6. (Optional) A caching, retrying credential source
Compose also wraps the lookup with a cache: one shared lookup for concurrent pulls, token reuse until it nearly expires, and a backoff after failures so it doesn't hit a hung or absent Desktop on every pull:
https://github.com/docker/compose/blob/e22de3b57415ea7bc3ddc6ce6ae2f547ea66993b/internal/registry/desktop.go#L132-L169
This is generic too. A wrapper such as
dockerhub.NewCachedAuth(auth, opts...)would be useful, but it is the most debatable item if the SDK wants to stay stateless.Proposal
Items 1–3 together could be one call that returns what a registry client needs:
With that, plus (4) and (5), the Compose side shrinks to building a
types.AuthConfigfromCredential.jwtExpiry,sessionExpiry,errNoUsername,errSessionExpired,isSessionUnavailable, the second profile lookup and the dependency onx/apiwould all go away. The matching Compose tests (TestJWTExpiry,…ExpiryFromTokenWhenClaimsLackIt,…UsernameFromProfile,TestIsSessionUnavailable) would move here.Happy to send PRs for any of these.