Skip to content

Commit 31efac8

Browse files
robmryclaude
andauthored
fix(image): map <token> credentials to IdentityToken in Pull (#151)
The Docker credential store convention uses "<token>" as the username to indicate the password field contains an identity/OAuth token rather than a literal password (see docker/cli credentials/native_store.go). When credentialsFn returns "<token>" as the username, map the password to AuthConfig.IdentityToken instead of AuthConfig.Password. This matches how the Docker CLI handles token credentials and ensures the daemon receives a properly structured auth config. Without this fix, the token is sent as a basic auth password, which causes containerd's transfer service to fail with 401 Unauthorized when authenticating against Docker Hub's token endpoint. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f7329ac commit 31efac8

2 files changed

Lines changed: 44 additions & 4 deletions

File tree

image/pull.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,22 @@ func Pull(ctx context.Context, imageName string, opts ...PullOption) error {
8787
pullOpts.client.Logger().Warn("failed to parse image reference, ServerAddress will be empty", "image", imageName, "error", err)
8888
}
8989

90-
authConfig := registry.AuthConfig{
91-
Username: username,
92-
Password: password,
93-
ServerAddress: imgRef.Registry,
90+
// The Docker credential store convention uses "<token>" as the username
91+
// to indicate the password is an identity/OAuth token, not a literal
92+
// password. Map this to the IdentityToken field so the daemon handles
93+
// it correctly (see docker/cli credentials/native_store.go).
94+
var authConfig registry.AuthConfig
95+
if username == "<token>" {
96+
authConfig = registry.AuthConfig{
97+
IdentityToken: password,
98+
ServerAddress: imgRef.Registry,
99+
}
100+
} else {
101+
authConfig = registry.AuthConfig{
102+
Username: username,
103+
Password: password,
104+
ServerAddress: imgRef.Registry,
105+
}
94106
}
95107

96108
pullOpts.pullOptions.RegistryAuth, err = authconfig.Encode(authConfig)

image/pull_unit_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,34 @@ func TestPullRegistryAuth(t *testing.T) {
4242
require.Equal(t, "myregistry.example.com", decoded.ServerAddress)
4343
}
4444

45+
func TestPullRegistryAuth_TokenUsername(t *testing.T) {
46+
mockCli := &errMockCli{}
47+
sdk, err := sdkclient.New(context.TODO(), sdkclient.WithDockerAPI(mockCli))
48+
require.NoError(t, err)
49+
50+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
51+
defer cancel()
52+
53+
imageName := "docker/sandbox-templates:shell-docker"
54+
err = Pull(ctx, imageName,
55+
WithPullOptions(dockerclient.ImagePullOptions{}),
56+
WithCredentialsFn(func(_ string) (string, string, error) {
57+
return "<token>", "my-oauth-token", nil
58+
}),
59+
WithPullClient(sdk),
60+
)
61+
require.NoError(t, err)
62+
63+
// When username is "<token>", the token should be mapped to IdentityToken
64+
// (not Username/Password), matching the Docker CLI credential store convention.
65+
decoded, err := authconfig.Decode(mockCli.lastPullOptions.RegistryAuth)
66+
require.NoError(t, err)
67+
require.Empty(t, decoded.Username, "Username should be empty for token auth")
68+
require.Empty(t, decoded.Password, "Password should be empty for token auth")
69+
require.Equal(t, "my-oauth-token", decoded.IdentityToken)
70+
require.Equal(t, "docker.io", decoded.ServerAddress)
71+
}
72+
4573
func TestPull(t *testing.T) {
4674
defaultPullOpts := []PullOption{WithPullOptions(dockerclient.ImagePullOptions{})}
4775

0 commit comments

Comments
 (0)