Browse code

api: move authconfig package from types/registry to pkg

Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>

Austin Vazquez authored on 2025/08/22 06:39:30
Showing 12 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,92 @@
0
+package authconfig
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/base64"
5
+	"encoding/json"
6
+	"errors"
7
+	"fmt"
8
+	"io"
9
+
10
+	"github.com/moby/moby/api/types/registry"
11
+)
12
+
13
+// Encode serializes the auth configuration as a base64url encoded
14
+// ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header.
15
+//
16
+// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
17
+func Encode(authConfig registry.AuthConfig) (string, error) {
18
+	// Older daemons (or registries) may not handle an empty string,
19
+	// which resulted in an "io.EOF" when unmarshaling or decoding.
20
+	//
21
+	// FIXME(thaJeztah): find exactly what code-paths are impacted by this.
22
+	// if authConfig == (AuthConfig{}) { return "", nil }
23
+	buf, err := json.Marshal(authConfig)
24
+	if err != nil {
25
+		return "", errInvalidParameter{err}
26
+	}
27
+	return base64.URLEncoding.EncodeToString(buf), nil
28
+}
29
+
30
+// Decode decodes base64url encoded ([RFC4648, section 5]) JSON
31
+// authentication information as sent through the X-Registry-Auth header.
32
+//
33
+// This function always returns an [AuthConfig], even if an error occurs. It is up
34
+// to the caller to decide if authentication is required, and if the error can
35
+// be ignored.
36
+//
37
+// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
38
+func Decode(authEncoded string) (*registry.AuthConfig, error) {
39
+	if authEncoded == "" {
40
+		return &registry.AuthConfig{}, nil
41
+	}
42
+
43
+	decoded, err := base64.URLEncoding.DecodeString(authEncoded)
44
+	if err != nil {
45
+		var e base64.CorruptInputError
46
+		if errors.As(err, &e) {
47
+			return &registry.AuthConfig{}, invalid(errors.New("must be a valid base64url-encoded string"))
48
+		}
49
+		return &registry.AuthConfig{}, invalid(err)
50
+	}
51
+
52
+	if bytes.Equal(decoded, []byte("{}")) {
53
+		return &registry.AuthConfig{}, nil
54
+	}
55
+
56
+	return decode(bytes.NewReader(decoded))
57
+}
58
+
59
+// DecodeRequestBody decodes authentication information as sent as JSON in the
60
+// body of a request. This function is to provide backward compatibility with old
61
+// clients and API versions. Current clients and API versions expect authentication
62
+// to be provided through the X-Registry-Auth header.
63
+//
64
+// Like [Decode], this function always returns an [AuthConfig], even if an
65
+// error occurs. It is up to the caller to decide if authentication is required,
66
+// and if the error can be ignored.
67
+func DecodeRequestBody(r io.ReadCloser) (*registry.AuthConfig, error) {
68
+	return decode(r)
69
+}
70
+
71
+func decode(r io.Reader) (*registry.AuthConfig, error) {
72
+	authConfig := &registry.AuthConfig{}
73
+	if err := json.NewDecoder(r).Decode(authConfig); err != nil {
74
+		// always return an (empty) AuthConfig to increase compatibility with
75
+		// the existing API.
76
+		return &registry.AuthConfig{}, invalid(fmt.Errorf("invalid JSON: %w", err))
77
+	}
78
+	return authConfig, nil
79
+}
80
+
81
+func invalid(err error) error {
82
+	return errInvalidParameter{fmt.Errorf("invalid X-Registry-Auth header: %w", err)}
83
+}
84
+
85
+type errInvalidParameter struct{ error }
86
+
87
+func (errInvalidParameter) InvalidParameter() {}
88
+
89
+func (e errInvalidParameter) Cause() error { return e.error }
90
+
91
+func (e errInvalidParameter) Unwrap() error { return e.error }
0 92
new file mode 100644
... ...
@@ -0,0 +1,191 @@
0
+package authconfig
1
+
2
+import (
3
+	"encoding/base64"
4
+	"strings"
5
+	"testing"
6
+
7
+	"github.com/moby/moby/api/types/registry"
8
+	"gotest.tools/v3/assert"
9
+	is "gotest.tools/v3/assert/cmp"
10
+)
11
+
12
+func TestDecodeAuthConfig(t *testing.T) {
13
+	tests := []struct {
14
+		doc         string
15
+		input       string
16
+		inputBase64 string
17
+		expected    registry.AuthConfig
18
+		expectedErr string
19
+	}{
20
+		{
21
+			doc:         "empty",
22
+			input:       ``,
23
+			inputBase64: ``,
24
+			expected:    registry.AuthConfig{},
25
+		},
26
+		{
27
+			doc:         "empty JSON",
28
+			input:       `{}`,
29
+			inputBase64: `e30=`,
30
+			expected:    registry.AuthConfig{},
31
+		},
32
+		{
33
+			doc:         "malformed JSON",
34
+			input:       `{`,
35
+			inputBase64: `ew==`,
36
+			expected:    registry.AuthConfig{},
37
+			expectedErr: `invalid X-Registry-Auth header: invalid JSON: unexpected EOF`,
38
+		},
39
+		{
40
+			doc:         "test authConfig",
41
+			input:       `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
42
+			inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ==`,
43
+			expected: registry.AuthConfig{
44
+				Username:      "testuser",
45
+				Password:      "testpassword",
46
+				ServerAddress: "example.com",
47
+			},
48
+		},
49
+		{
50
+			// FIXME(thaJeztah): we should not accept multiple JSON documents.
51
+			doc:         "multiple authConfig",
52
+			input:       `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}{"username":"testuser2","password":"testpassword2","serveraddress":"example.org"}`,
53
+			inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifXsidXNlcm5hbWUiOiJ0ZXN0dXNlcjIiLCJwYXNzd29yZCI6InRlc3RwYXNzd29yZDIiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5vcmcifQ==`,
54
+			expected: registry.AuthConfig{
55
+				Username:      "testuser",
56
+				Password:      "testpassword",
57
+				ServerAddress: "example.com",
58
+			},
59
+		},
60
+		// We currently only support base64url encoding with padding, so
61
+		// un-padded should produce an error.
62
+		//
63
+		// RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5
64
+		// RFC4648, section 3.2: https://tools.ietf.org/html/rfc4648#section-3.2
65
+		{
66
+			doc:         "empty JSON no padding",
67
+			input:       `{}`,
68
+			inputBase64: `e30`,
69
+			expected:    registry.AuthConfig{},
70
+			expectedErr: `invalid X-Registry-Auth header: must be a valid base64url-encoded string`,
71
+		},
72
+		{
73
+			doc:         "test authConfig",
74
+			input:       `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
75
+			inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ`,
76
+			expected:    registry.AuthConfig{},
77
+			expectedErr: `invalid X-Registry-Auth header: must be a valid base64url-encoded string`,
78
+		},
79
+	}
80
+
81
+	for _, tc := range tests {
82
+		t.Run(tc.doc, func(t *testing.T) {
83
+			if tc.inputBase64 != "" {
84
+				// Sanity check to make sure our fixtures are correct.
85
+				b64 := base64.URLEncoding.EncodeToString([]byte(tc.input))
86
+				if !strings.HasSuffix(tc.inputBase64, "=") {
87
+					b64 = strings.TrimRight(b64, "=")
88
+				}
89
+				assert.Check(t, is.Equal(b64, tc.inputBase64))
90
+			}
91
+
92
+			out, err := Decode(tc.inputBase64)
93
+			if tc.expectedErr != "" {
94
+				assert.Check(t, is.ErrorType(err, errInvalidParameter{}))
95
+				assert.Check(t, is.Error(err, tc.expectedErr))
96
+			} else {
97
+				assert.NilError(t, err)
98
+				assert.Equal(t, *out, tc.expected)
99
+			}
100
+		})
101
+	}
102
+}
103
+
104
+func TestEncodeAuthConfig(t *testing.T) {
105
+	tests := []struct {
106
+		doc       string
107
+		input     registry.AuthConfig
108
+		outBase64 string
109
+		outPlain  string
110
+	}{
111
+		{
112
+			// Older daemons (or registries) may not handle an empty string,
113
+			// which resulted in an "io.EOF" when unmarshaling or decoding.
114
+			//
115
+			// FIXME(thaJeztah): find exactly what code-paths are impacted by this.
116
+			doc:       "empty",
117
+			input:     registry.AuthConfig{},
118
+			outBase64: `e30=`,
119
+			outPlain:  `{}`,
120
+		},
121
+		{
122
+			doc: "test authConfig",
123
+			input: registry.AuthConfig{
124
+				Username:      "testuser",
125
+				Password:      "testpassword",
126
+				ServerAddress: "example.com",
127
+			},
128
+			outBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ==`,
129
+			outPlain:  `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
130
+		},
131
+	}
132
+	for _, tc := range tests {
133
+		// Sanity check to make sure our fixtures are correct.
134
+		b64 := base64.URLEncoding.EncodeToString([]byte(tc.outPlain))
135
+		assert.Check(t, is.Equal(b64, tc.outBase64))
136
+
137
+		t.Run(tc.doc, func(t *testing.T) {
138
+			out, err := Encode(tc.input)
139
+			assert.NilError(t, err)
140
+			assert.Equal(t, out, tc.outBase64)
141
+
142
+			authJSON, err := base64.URLEncoding.DecodeString(out)
143
+			assert.NilError(t, err)
144
+			assert.Equal(t, string(authJSON), tc.outPlain)
145
+		})
146
+	}
147
+}
148
+
149
+func BenchmarkDecodeAuthConfig(b *testing.B) {
150
+	cases := []struct {
151
+		doc         string
152
+		inputBase64 string
153
+		invalid     bool
154
+	}{
155
+		{
156
+			doc:         "empty",
157
+			inputBase64: ``,
158
+		},
159
+		{
160
+			doc:         "empty JSON",
161
+			inputBase64: `e30=`,
162
+		},
163
+		{
164
+			doc:         "valid",
165
+			inputBase64: base64.URLEncoding.EncodeToString([]byte(`{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`)),
166
+		},
167
+		{
168
+			doc:         "invalid base64",
169
+			inputBase64: "not-base64",
170
+			invalid:     true,
171
+		},
172
+		{
173
+			doc:         "malformed JSON",
174
+			inputBase64: `ew==`,
175
+			invalid:     true,
176
+		},
177
+	}
178
+
179
+	for _, tc := range cases {
180
+		b.Run(tc.doc, func(b *testing.B) {
181
+			b.ReportAllocs()
182
+			for i := 0; i < b.N; i++ {
183
+				_, err := Decode(tc.inputBase64)
184
+				if !tc.invalid && err != nil {
185
+					b.Fatal(err)
186
+				}
187
+			}
188
+		})
189
+	}
190
+}
... ...
@@ -1,14 +1,6 @@
1 1
 package registry
2 2
 
3
-import (
4
-	"bytes"
5
-	"context"
6
-	"encoding/base64"
7
-	"encoding/json"
8
-	"errors"
9
-	"fmt"
10
-	"io"
11
-)
3
+import "context"
12 4
 
13 5
 // AuthHeader is the name of the header used to send encoded registry
14 6
 // authorization credentials for registry operations (push/pull).
... ...
@@ -46,85 +38,3 @@ type AuthConfig struct {
46 46
 	// RegistryToken is a bearer token to be sent to a registry
47 47
 	RegistryToken string `json:"registrytoken,omitempty"`
48 48
 }
49
-
50
-// EncodeAuthConfig serializes the auth configuration as a base64url encoded
51
-// ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header.
52
-//
53
-// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
54
-func EncodeAuthConfig(authConfig AuthConfig) (string, error) {
55
-	// Older daemons (or registries) may not handle an empty string,
56
-	// which resulted in an "io.EOF" when unmarshaling or decoding.
57
-	//
58
-	// FIXME(thaJeztah): find exactly what code-paths are impacted by this.
59
-	// if authConfig == (AuthConfig{}) { return "", nil }
60
-	buf, err := json.Marshal(authConfig)
61
-	if err != nil {
62
-		return "", errInvalidParameter{err}
63
-	}
64
-	return base64.URLEncoding.EncodeToString(buf), nil
65
-}
66
-
67
-// DecodeAuthConfig decodes base64url encoded ([RFC4648, section 5]) JSON
68
-// authentication information as sent through the X-Registry-Auth header.
69
-//
70
-// This function always returns an [AuthConfig], even if an error occurs. It is up
71
-// to the caller to decide if authentication is required, and if the error can
72
-// be ignored.
73
-//
74
-// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
75
-func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) {
76
-	if authEncoded == "" {
77
-		return &AuthConfig{}, nil
78
-	}
79
-
80
-	decoded, err := base64.URLEncoding.DecodeString(authEncoded)
81
-	if err != nil {
82
-		var e base64.CorruptInputError
83
-		if errors.As(err, &e) {
84
-			return &AuthConfig{}, invalid(errors.New("must be a valid base64url-encoded string"))
85
-		}
86
-		return &AuthConfig{}, invalid(err)
87
-	}
88
-
89
-	if bytes.Equal(decoded, []byte("{}")) {
90
-		return &AuthConfig{}, nil
91
-	}
92
-
93
-	return decodeAuthConfigFromReader(bytes.NewReader(decoded))
94
-}
95
-
96
-// DecodeAuthConfigBody decodes authentication information as sent as JSON in the
97
-// body of a request. This function is to provide backward compatibility with old
98
-// clients and API versions. Current clients and API versions expect authentication
99
-// to be provided through the X-Registry-Auth header.
100
-//
101
-// Like [DecodeAuthConfig], this function always returns an [AuthConfig], even if an
102
-// error occurs. It is up to the caller to decide if authentication is required,
103
-// and if the error can be ignored.
104
-//
105
-// Deprecated: this function is no longer used and will be removed in the next release.
106
-func DecodeAuthConfigBody(rdr io.ReadCloser) (*AuthConfig, error) {
107
-	return decodeAuthConfigFromReader(rdr)
108
-}
109
-
110
-func decodeAuthConfigFromReader(rdr io.Reader) (*AuthConfig, error) {
111
-	authConfig := &AuthConfig{}
112
-	if err := json.NewDecoder(rdr).Decode(authConfig); err != nil {
113
-		// always return an (empty) AuthConfig to increase compatibility with
114
-		// the existing API.
115
-		return &AuthConfig{}, invalid(fmt.Errorf("invalid JSON: %w", err))
116
-	}
117
-	return authConfig, nil
118
-}
119
-
120
-func invalid(err error) error {
121
-	return errInvalidParameter{fmt.Errorf("invalid X-Registry-Auth header: %w", err)}
122
-}
123
-
124
-type errInvalidParameter struct{ error }
125
-
126
-func (errInvalidParameter) InvalidParameter() {}
127
-
128
-func (e errInvalidParameter) Cause() error { return e.error }
129
-
130
-func (e errInvalidParameter) Unwrap() error { return e.error }
131 49
deleted file mode 100644
... ...
@@ -1,190 +0,0 @@
1
-package registry
2
-
3
-import (
4
-	"encoding/base64"
5
-	"strings"
6
-	"testing"
7
-
8
-	"gotest.tools/v3/assert"
9
-	is "gotest.tools/v3/assert/cmp"
10
-)
11
-
12
-func TestDecodeAuthConfig(t *testing.T) {
13
-	tests := []struct {
14
-		doc         string
15
-		input       string
16
-		inputBase64 string
17
-		expected    AuthConfig
18
-		expectedErr string
19
-	}{
20
-		{
21
-			doc:         "empty",
22
-			input:       ``,
23
-			inputBase64: ``,
24
-			expected:    AuthConfig{},
25
-		},
26
-		{
27
-			doc:         "empty JSON",
28
-			input:       `{}`,
29
-			inputBase64: `e30=`,
30
-			expected:    AuthConfig{},
31
-		},
32
-		{
33
-			doc:         "malformed JSON",
34
-			input:       `{`,
35
-			inputBase64: `ew==`,
36
-			expected:    AuthConfig{},
37
-			expectedErr: `invalid X-Registry-Auth header: invalid JSON: unexpected EOF`,
38
-		},
39
-		{
40
-			doc:         "test authConfig",
41
-			input:       `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
42
-			inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ==`,
43
-			expected: AuthConfig{
44
-				Username:      "testuser",
45
-				Password:      "testpassword",
46
-				ServerAddress: "example.com",
47
-			},
48
-		},
49
-		{
50
-			// FIXME(thaJeztah): we should not accept multiple JSON documents.
51
-			doc:         "multiple authConfig",
52
-			input:       `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}{"username":"testuser2","password":"testpassword2","serveraddress":"example.org"}`,
53
-			inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifXsidXNlcm5hbWUiOiJ0ZXN0dXNlcjIiLCJwYXNzd29yZCI6InRlc3RwYXNzd29yZDIiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5vcmcifQ==`,
54
-			expected: AuthConfig{
55
-				Username:      "testuser",
56
-				Password:      "testpassword",
57
-				ServerAddress: "example.com",
58
-			},
59
-		},
60
-		// We currently only support base64url encoding with padding, so
61
-		// un-padded should produce an error.
62
-		//
63
-		// RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5
64
-		// RFC4648, section 3.2: https://tools.ietf.org/html/rfc4648#section-3.2
65
-		{
66
-			doc:         "empty JSON no padding",
67
-			input:       `{}`,
68
-			inputBase64: `e30`,
69
-			expected:    AuthConfig{},
70
-			expectedErr: `invalid X-Registry-Auth header: must be a valid base64url-encoded string`,
71
-		},
72
-		{
73
-			doc:         "test authConfig",
74
-			input:       `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
75
-			inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ`,
76
-			expected:    AuthConfig{},
77
-			expectedErr: `invalid X-Registry-Auth header: must be a valid base64url-encoded string`,
78
-		},
79
-	}
80
-
81
-	for _, tc := range tests {
82
-		t.Run(tc.doc, func(t *testing.T) {
83
-			if tc.inputBase64 != "" {
84
-				// Sanity check to make sure our fixtures are correct.
85
-				b64 := base64.URLEncoding.EncodeToString([]byte(tc.input))
86
-				if !strings.HasSuffix(tc.inputBase64, "=") {
87
-					b64 = strings.TrimRight(b64, "=")
88
-				}
89
-				assert.Check(t, is.Equal(b64, tc.inputBase64))
90
-			}
91
-
92
-			out, err := DecodeAuthConfig(tc.inputBase64)
93
-			if tc.expectedErr != "" {
94
-				assert.Check(t, is.ErrorType(err, errInvalidParameter{}))
95
-				assert.Check(t, is.Error(err, tc.expectedErr))
96
-			} else {
97
-				assert.NilError(t, err)
98
-				assert.Equal(t, *out, tc.expected)
99
-			}
100
-		})
101
-	}
102
-}
103
-
104
-func TestEncodeAuthConfig(t *testing.T) {
105
-	tests := []struct {
106
-		doc       string
107
-		input     AuthConfig
108
-		outBase64 string
109
-		outPlain  string
110
-	}{
111
-		{
112
-			// Older daemons (or registries) may not handle an empty string,
113
-			// which resulted in an "io.EOF" when unmarshaling or decoding.
114
-			//
115
-			// FIXME(thaJeztah): find exactly what code-paths are impacted by this.
116
-			doc:       "empty",
117
-			input:     AuthConfig{},
118
-			outBase64: `e30=`,
119
-			outPlain:  `{}`,
120
-		},
121
-		{
122
-			doc: "test authConfig",
123
-			input: AuthConfig{
124
-				Username:      "testuser",
125
-				Password:      "testpassword",
126
-				ServerAddress: "example.com",
127
-			},
128
-			outBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ==`,
129
-			outPlain:  `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`,
130
-		},
131
-	}
132
-	for _, tc := range tests {
133
-		// Sanity check to make sure our fixtures are correct.
134
-		b64 := base64.URLEncoding.EncodeToString([]byte(tc.outPlain))
135
-		assert.Check(t, is.Equal(b64, tc.outBase64))
136
-
137
-		t.Run(tc.doc, func(t *testing.T) {
138
-			out, err := EncodeAuthConfig(tc.input)
139
-			assert.NilError(t, err)
140
-			assert.Equal(t, out, tc.outBase64)
141
-
142
-			authJSON, err := base64.URLEncoding.DecodeString(out)
143
-			assert.NilError(t, err)
144
-			assert.Equal(t, string(authJSON), tc.outPlain)
145
-		})
146
-	}
147
-}
148
-
149
-func BenchmarkDecodeAuthConfig(b *testing.B) {
150
-	cases := []struct {
151
-		doc         string
152
-		inputBase64 string
153
-		invalid     bool
154
-	}{
155
-		{
156
-			doc:         "empty",
157
-			inputBase64: ``,
158
-		},
159
-		{
160
-			doc:         "empty JSON",
161
-			inputBase64: `e30=`,
162
-		},
163
-		{
164
-			doc:         "valid",
165
-			inputBase64: base64.URLEncoding.EncodeToString([]byte(`{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`)),
166
-		},
167
-		{
168
-			doc:         "invalid base64",
169
-			inputBase64: "not-base64",
170
-			invalid:     true,
171
-		},
172
-		{
173
-			doc:         "malformed JSON",
174
-			inputBase64: `ew==`,
175
-			invalid:     true,
176
-		},
177
-	}
178
-
179
-	for _, tc := range cases {
180
-		b.Run(tc.doc, func(b *testing.B) {
181
-			b.ReportAllocs()
182
-			for i := 0; i < b.N; i++ {
183
-				_, err := DecodeAuthConfig(tc.inputBase64)
184
-				if !tc.invalid && err != nil {
185
-					b.Fatal(err)
186
-				}
187
-			}
188
-		})
189
-	}
190
-}
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	"github.com/containerd/log"
12 12
 	"github.com/distribution/reference"
13 13
 	gogotypes "github.com/gogo/protobuf/types"
14
+	"github.com/moby/moby/api/pkg/authconfig"
14 15
 	"github.com/moby/moby/api/types/container"
15 16
 	"github.com/moby/moby/api/types/registry"
16 17
 	"github.com/moby/moby/api/types/swarm"
... ...
@@ -230,7 +231,7 @@ func (c *Cluster) CreateService(s swarm.ServiceSpec, encodedAuth string, queryRe
230 230
 			authConfig := &registry.AuthConfig{}
231 231
 			if encodedAuth != "" {
232 232
 				var err error
233
-				authConfig, err = registry.DecodeAuthConfig(encodedAuth)
233
+				authConfig, err = authconfig.Decode(encodedAuth)
234 234
 				if err != nil {
235 235
 					log.G(ctx).Warnf("invalid authconfig: %v", err)
236 236
 				}
... ...
@@ -348,7 +349,7 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec swa
348 348
 			authConfig := &registry.AuthConfig{}
349 349
 			if encodedAuth != "" {
350 350
 				var err error
351
-				authConfig, err = registry.DecodeAuthConfig(encodedAuth)
351
+				authConfig, err = authconfig.Decode(encodedAuth)
352 352
 				if err != nil {
353 353
 					log.G(ctx).Warnf("invalid authconfig: %v", err)
354 354
 				}
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"github.com/docker/distribution"
10 10
 	"github.com/docker/distribution/manifest/manifestlist"
11 11
 	"github.com/docker/distribution/manifest/schema2"
12
+	"github.com/moby/moby/api/pkg/authconfig"
12 13
 	"github.com/moby/moby/api/types/registry"
13 14
 	distributionpkg "github.com/moby/moby/v2/daemon/internal/distribution"
14 15
 	"github.com/moby/moby/v2/daemon/server/httputils"
... ...
@@ -42,7 +43,7 @@ func (dr *distributionRouter) getDistributionInfo(ctx context.Context, w http.Re
42 42
 
43 43
 	// For a search it is not an error if no auth was given. Ignore invalid
44 44
 	// AuthConfig to increase compatibility with the existing API.
45
-	authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
45
+	authConfig, _ := authconfig.Decode(r.Header.Get(registry.AuthHeader))
46 46
 	repos, err := dr.backend.GetRepositories(ctx, namedRef, authConfig)
47 47
 	if err != nil {
48 48
 		return err
... ...
@@ -12,6 +12,7 @@ import (
12 12
 
13 13
 	"github.com/containerd/platforms"
14 14
 	"github.com/distribution/reference"
15
+	"github.com/moby/moby/api/pkg/authconfig"
15 16
 	"github.com/moby/moby/api/pkg/progress"
16 17
 	"github.com/moby/moby/api/pkg/streamformatter"
17 18
 	"github.com/moby/moby/api/types/filters"
... ...
@@ -101,7 +102,7 @@ func (ir *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrit
101 101
 		// AuthConfig to increase compatibility with the existing API.
102 102
 		//
103 103
 		// TODO(thaJeztah): accept empty values but return an error when failing to decode.
104
-		authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
104
+		authConfig, _ := authconfig.Decode(r.Header.Get(registry.AuthHeader))
105 105
 		progressErr = ir.backend.PullImage(ctx, ref, platform, metaHeaders, authConfig, output)
106 106
 	} else { // import
107 107
 		src := r.Form.Get("fromSrc")
... ...
@@ -170,7 +171,7 @@ func (ir *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter
170 170
 	// to increase compatibility with the existing API.
171 171
 	//
172 172
 	// TODO(thaJeztah): accept empty values but return an error when failing to decode.
173
-	authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
173
+	authConfig, _ := authconfig.Decode(r.Header.Get(registry.AuthHeader))
174 174
 
175 175
 	output := ioutils.NewWriteFlusher(w)
176 176
 	defer output.Close()
... ...
@@ -554,7 +555,7 @@ func (ir *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWrite
554 554
 
555 555
 	// For a search it is not an error if no auth was given. Ignore invalid
556 556
 	// AuthConfig to increase compatibility with the existing API.
557
-	authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
557
+	authConfig, _ := authconfig.Decode(r.Header.Get(registry.AuthHeader))
558 558
 
559 559
 	headers := http.Header{}
560 560
 	for k, v := range r.Header {
... ...
@@ -7,6 +7,7 @@ import (
7 7
 	"strings"
8 8
 
9 9
 	"github.com/distribution/reference"
10
+	"github.com/moby/moby/api/pkg/authconfig"
10 11
 	"github.com/moby/moby/api/pkg/streamformatter"
11 12
 	"github.com/moby/moby/api/types/filters"
12 13
 	"github.com/moby/moby/api/types/plugin"
... ...
@@ -26,7 +27,7 @@ func parseHeaders(headers http.Header) (map[string][]string, *registry.AuthConfi
26 26
 	}
27 27
 
28 28
 	// Ignore invalid AuthConfig to increase compatibility with the existing API.
29
-	authConfig, _ := registry.DecodeAuthConfig(headers.Get(registry.AuthHeader))
29
+	authConfig, _ := authconfig.Decode(headers.Get(registry.AuthHeader))
30 30
 	return metaHeaders, authConfig
31 31
 }
32 32
 
... ...
@@ -8,6 +8,7 @@ import (
8 8
 	"time"
9 9
 
10 10
 	"github.com/containerd/log"
11
+	"github.com/moby/moby/api/pkg/authconfig"
11 12
 	buildtypes "github.com/moby/moby/api/types/build"
12 13
 	"github.com/moby/moby/api/types/events"
13 14
 	"github.com/moby/moby/api/types/filters"
... ...
@@ -373,9 +374,7 @@ func (s *systemRouter) getEvents(ctx context.Context, w http.ResponseWriter, r *
373 373
 }
374 374
 
375 375
 func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
376
-	var config *registry.AuthConfig
377
-	err := json.NewDecoder(r.Body).Decode(&config)
378
-	r.Body.Close()
376
+	config, err := authconfig.DecodeRequestBody(r.Body)
379 377
 	if err != nil {
380 378
 		return err
381 379
 	}
382 380
new file mode 100644
... ...
@@ -0,0 +1,92 @@
0
+package authconfig
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/base64"
5
+	"encoding/json"
6
+	"errors"
7
+	"fmt"
8
+	"io"
9
+
10
+	"github.com/moby/moby/api/types/registry"
11
+)
12
+
13
+// Encode serializes the auth configuration as a base64url encoded
14
+// ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header.
15
+//
16
+// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
17
+func Encode(authConfig registry.AuthConfig) (string, error) {
18
+	// Older daemons (or registries) may not handle an empty string,
19
+	// which resulted in an "io.EOF" when unmarshaling or decoding.
20
+	//
21
+	// FIXME(thaJeztah): find exactly what code-paths are impacted by this.
22
+	// if authConfig == (AuthConfig{}) { return "", nil }
23
+	buf, err := json.Marshal(authConfig)
24
+	if err != nil {
25
+		return "", errInvalidParameter{err}
26
+	}
27
+	return base64.URLEncoding.EncodeToString(buf), nil
28
+}
29
+
30
+// Decode decodes base64url encoded ([RFC4648, section 5]) JSON
31
+// authentication information as sent through the X-Registry-Auth header.
32
+//
33
+// This function always returns an [AuthConfig], even if an error occurs. It is up
34
+// to the caller to decide if authentication is required, and if the error can
35
+// be ignored.
36
+//
37
+// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
38
+func Decode(authEncoded string) (*registry.AuthConfig, error) {
39
+	if authEncoded == "" {
40
+		return &registry.AuthConfig{}, nil
41
+	}
42
+
43
+	decoded, err := base64.URLEncoding.DecodeString(authEncoded)
44
+	if err != nil {
45
+		var e base64.CorruptInputError
46
+		if errors.As(err, &e) {
47
+			return &registry.AuthConfig{}, invalid(errors.New("must be a valid base64url-encoded string"))
48
+		}
49
+		return &registry.AuthConfig{}, invalid(err)
50
+	}
51
+
52
+	if bytes.Equal(decoded, []byte("{}")) {
53
+		return &registry.AuthConfig{}, nil
54
+	}
55
+
56
+	return decode(bytes.NewReader(decoded))
57
+}
58
+
59
+// DecodeRequestBody decodes authentication information as sent as JSON in the
60
+// body of a request. This function is to provide backward compatibility with old
61
+// clients and API versions. Current clients and API versions expect authentication
62
+// to be provided through the X-Registry-Auth header.
63
+//
64
+// Like [Decode], this function always returns an [AuthConfig], even if an
65
+// error occurs. It is up to the caller to decide if authentication is required,
66
+// and if the error can be ignored.
67
+func DecodeRequestBody(r io.ReadCloser) (*registry.AuthConfig, error) {
68
+	return decode(r)
69
+}
70
+
71
+func decode(r io.Reader) (*registry.AuthConfig, error) {
72
+	authConfig := &registry.AuthConfig{}
73
+	if err := json.NewDecoder(r).Decode(authConfig); err != nil {
74
+		// always return an (empty) AuthConfig to increase compatibility with
75
+		// the existing API.
76
+		return &registry.AuthConfig{}, invalid(fmt.Errorf("invalid JSON: %w", err))
77
+	}
78
+	return authConfig, nil
79
+}
80
+
81
+func invalid(err error) error {
82
+	return errInvalidParameter{fmt.Errorf("invalid X-Registry-Auth header: %w", err)}
83
+}
84
+
85
+type errInvalidParameter struct{ error }
86
+
87
+func (errInvalidParameter) InvalidParameter() {}
88
+
89
+func (e errInvalidParameter) Cause() error { return e.error }
90
+
91
+func (e errInvalidParameter) Unwrap() error { return e.error }
... ...
@@ -1,14 +1,6 @@
1 1
 package registry
2 2
 
3
-import (
4
-	"bytes"
5
-	"context"
6
-	"encoding/base64"
7
-	"encoding/json"
8
-	"errors"
9
-	"fmt"
10
-	"io"
11
-)
3
+import "context"
12 4
 
13 5
 // AuthHeader is the name of the header used to send encoded registry
14 6
 // authorization credentials for registry operations (push/pull).
... ...
@@ -46,85 +38,3 @@ type AuthConfig struct {
46 46
 	// RegistryToken is a bearer token to be sent to a registry
47 47
 	RegistryToken string `json:"registrytoken,omitempty"`
48 48
 }
49
-
50
-// EncodeAuthConfig serializes the auth configuration as a base64url encoded
51
-// ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header.
52
-//
53
-// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
54
-func EncodeAuthConfig(authConfig AuthConfig) (string, error) {
55
-	// Older daemons (or registries) may not handle an empty string,
56
-	// which resulted in an "io.EOF" when unmarshaling or decoding.
57
-	//
58
-	// FIXME(thaJeztah): find exactly what code-paths are impacted by this.
59
-	// if authConfig == (AuthConfig{}) { return "", nil }
60
-	buf, err := json.Marshal(authConfig)
61
-	if err != nil {
62
-		return "", errInvalidParameter{err}
63
-	}
64
-	return base64.URLEncoding.EncodeToString(buf), nil
65
-}
66
-
67
-// DecodeAuthConfig decodes base64url encoded ([RFC4648, section 5]) JSON
68
-// authentication information as sent through the X-Registry-Auth header.
69
-//
70
-// This function always returns an [AuthConfig], even if an error occurs. It is up
71
-// to the caller to decide if authentication is required, and if the error can
72
-// be ignored.
73
-//
74
-// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5
75
-func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) {
76
-	if authEncoded == "" {
77
-		return &AuthConfig{}, nil
78
-	}
79
-
80
-	decoded, err := base64.URLEncoding.DecodeString(authEncoded)
81
-	if err != nil {
82
-		var e base64.CorruptInputError
83
-		if errors.As(err, &e) {
84
-			return &AuthConfig{}, invalid(errors.New("must be a valid base64url-encoded string"))
85
-		}
86
-		return &AuthConfig{}, invalid(err)
87
-	}
88
-
89
-	if bytes.Equal(decoded, []byte("{}")) {
90
-		return &AuthConfig{}, nil
91
-	}
92
-
93
-	return decodeAuthConfigFromReader(bytes.NewReader(decoded))
94
-}
95
-
96
-// DecodeAuthConfigBody decodes authentication information as sent as JSON in the
97
-// body of a request. This function is to provide backward compatibility with old
98
-// clients and API versions. Current clients and API versions expect authentication
99
-// to be provided through the X-Registry-Auth header.
100
-//
101
-// Like [DecodeAuthConfig], this function always returns an [AuthConfig], even if an
102
-// error occurs. It is up to the caller to decide if authentication is required,
103
-// and if the error can be ignored.
104
-//
105
-// Deprecated: this function is no longer used and will be removed in the next release.
106
-func DecodeAuthConfigBody(rdr io.ReadCloser) (*AuthConfig, error) {
107
-	return decodeAuthConfigFromReader(rdr)
108
-}
109
-
110
-func decodeAuthConfigFromReader(rdr io.Reader) (*AuthConfig, error) {
111
-	authConfig := &AuthConfig{}
112
-	if err := json.NewDecoder(rdr).Decode(authConfig); err != nil {
113
-		// always return an (empty) AuthConfig to increase compatibility with
114
-		// the existing API.
115
-		return &AuthConfig{}, invalid(fmt.Errorf("invalid JSON: %w", err))
116
-	}
117
-	return authConfig, nil
118
-}
119
-
120
-func invalid(err error) error {
121
-	return errInvalidParameter{fmt.Errorf("invalid X-Registry-Auth header: %w", err)}
122
-}
123
-
124
-type errInvalidParameter struct{ error }
125
-
126
-func (errInvalidParameter) InvalidParameter() {}
127
-
128
-func (e errInvalidParameter) Cause() error { return e.error }
129
-
130
-func (e errInvalidParameter) Unwrap() error { return e.error }
... ...
@@ -944,6 +944,7 @@ github.com/moby/locker
944 944
 # github.com/moby/moby/api v1.52.0-alpha.1 => ./api
945 945
 ## explicit; go 1.23.0
946 946
 github.com/moby/moby/api
947
+github.com/moby/moby/api/pkg/authconfig
947 948
 github.com/moby/moby/api/pkg/progress
948 949
 github.com/moby/moby/api/pkg/stdcopy
949 950
 github.com/moby/moby/api/pkg/streamformatter