1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package transport
16
17 import (
18 "context"
19 "encoding/json"
20 "errors"
21 "fmt"
22 "io"
23 "net"
24 "net/http"
25 "net/url"
26 "strings"
27
28 authchallenge "github.com/docker/distribution/registry/client/auth/challenge"
29 "github.com/google/go-containerregistry/internal/redact"
30 "github.com/google/go-containerregistry/pkg/authn"
31 "github.com/google/go-containerregistry/pkg/logs"
32 "github.com/google/go-containerregistry/pkg/name"
33 )
34
35 type Token struct {
36 Token string `json:"token"`
37 AccessToken string `json:"access_token,omitempty"`
38 RefreshToken string `json:"refresh_token"`
39 ExpiresIn int `json:"expires_in"`
40 }
41
42
43 func Exchange(ctx context.Context, reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scopes []string, pr *Challenge) (*Token, error) {
44 if strings.ToLower(pr.Scheme) != "bearer" {
45
46 return nil, fmt.Errorf("challenge scheme %q is not bearer", pr.Scheme)
47 }
48 bt, err := fromChallenge(reg, auth, t, pr, scopes...)
49 if err != nil {
50 return nil, err
51 }
52 authcfg, err := auth.Authorization()
53 if err != nil {
54 return nil, err
55 }
56 tok, err := bt.Refresh(ctx, authcfg)
57 if err != nil {
58 return nil, err
59 }
60 return tok, nil
61 }
62
63
64 func FromToken(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, pr *Challenge, tok *Token) (http.RoundTripper, error) {
65 if strings.ToLower(pr.Scheme) != "bearer" {
66 return &Wrapper{&basicTransport{inner: t, auth: auth, target: reg.RegistryStr()}}, nil
67 }
68 bt, err := fromChallenge(reg, auth, t, pr)
69 if err != nil {
70 return nil, err
71 }
72 if tok.Token != "" {
73 bt.bearer.RegistryToken = tok.Token
74 }
75 return &Wrapper{bt}, nil
76 }
77
78 func fromChallenge(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, pr *Challenge, scopes ...string) (*bearerTransport, error) {
79
80 realm, ok := pr.Parameters["realm"]
81 if !ok {
82 return nil, fmt.Errorf("malformed www-authenticate, missing realm: %v", pr.Parameters)
83 }
84 service := pr.Parameters["service"]
85 scheme := "https"
86 if pr.Insecure {
87 scheme = "http"
88 }
89 return &bearerTransport{
90 inner: t,
91 basic: auth,
92 realm: realm,
93 registry: reg,
94 service: service,
95 scopes: scopes,
96 scheme: scheme,
97 }, nil
98 }
99
100 type bearerTransport struct {
101
102 inner http.RoundTripper
103
104 basic authn.Authenticator
105
106 bearer authn.AuthConfig
107
108 registry name.Registry
109
110 realm string
111
112 service string
113 scopes []string
114
115 scheme string
116 }
117
118 var _ http.RoundTripper = (*bearerTransport)(nil)
119
120 var portMap = map[string]string{
121 "http": "80",
122 "https": "443",
123 }
124
125 func stringSet(ss []string) map[string]struct{} {
126 set := make(map[string]struct{})
127 for _, s := range ss {
128 set[s] = struct{}{}
129 }
130 return set
131 }
132
133
134 func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
135 sendRequest := func() (*http.Response, error) {
136
137
138
139
140
141 if matchesHost(bt.registry.RegistryStr(), in, bt.scheme) {
142 hdr := fmt.Sprintf("Bearer %s", bt.bearer.RegistryToken)
143 in.Header.Set("Authorization", hdr)
144 }
145 return bt.inner.RoundTrip(in)
146 }
147
148 res, err := sendRequest()
149 if err != nil {
150 return nil, err
151 }
152
153
154 if challenges := authchallenge.ResponseChallenges(res); len(challenges) != 0 {
155
156 res.Body.Close()
157
158 newScopes := []string{}
159 for _, wac := range challenges {
160
161 if want, ok := wac.Parameters["scope"]; ok {
162
163 got := stringSet(bt.scopes)
164 if _, ok := got[want]; !ok {
165 newScopes = append(newScopes, want)
166 }
167 }
168 }
169
170
171
172
173 newScopes = append(newScopes, bt.scopes...)
174 bt.scopes = newScopes
175
176
177
178
179 if err = bt.refresh(in.Context()); err != nil {
180 return nil, err
181 }
182 return sendRequest()
183 }
184
185 return res, err
186 }
187
188
189
190
191
192 func (bt *bearerTransport) refresh(ctx context.Context) error {
193 auth, err := bt.basic.Authorization()
194 if err != nil {
195 return err
196 }
197
198 if auth.RegistryToken != "" {
199 bt.bearer.RegistryToken = auth.RegistryToken
200 return nil
201 }
202
203 response, err := bt.Refresh(ctx, auth)
204 if err != nil {
205 return err
206 }
207
208
209 if response.AccessToken != "" {
210 response.Token = response.AccessToken
211 }
212
213
214 if response.Token != "" {
215 bt.bearer.RegistryToken = response.Token
216 }
217
218
219 if response.RefreshToken != "" {
220 bt.basic = authn.FromConfig(authn.AuthConfig{
221 IdentityToken: response.RefreshToken,
222 })
223 }
224
225 return nil
226 }
227
228 func (bt *bearerTransport) Refresh(ctx context.Context, auth *authn.AuthConfig) (*Token, error) {
229 var (
230 content []byte
231 err error
232 )
233 if auth.IdentityToken != "" {
234
235
236
237 content, err = bt.refreshOauth(ctx)
238 var terr *Error
239 if errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound {
240
241
242
243 content, err = bt.refreshBasic(ctx)
244 }
245 } else {
246 content, err = bt.refreshBasic(ctx)
247 }
248 if err != nil {
249 return nil, err
250 }
251
252 var response Token
253 if err := json.Unmarshal(content, &response); err != nil {
254 return nil, err
255 }
256
257 if response.Token == "" && response.AccessToken == "" {
258 return &response, fmt.Errorf("no token in bearer response:\n%s", content)
259 }
260
261 return &response, nil
262 }
263
264 func matchesHost(host string, in *http.Request, scheme string) bool {
265 canonicalHeaderHost := canonicalAddress(in.Host, scheme)
266 canonicalURLHost := canonicalAddress(in.URL.Host, scheme)
267 canonicalRegistryHost := canonicalAddress(host, scheme)
268 return canonicalHeaderHost == canonicalRegistryHost || canonicalURLHost == canonicalRegistryHost
269 }
270
271 func canonicalAddress(host, scheme string) (address string) {
272
273
274
275
276
277
278
279
280
281 if strings.Count(host, ":") == 1 || (strings.Count(host, ":") >= 2 && strings.Contains(host, "]:")) {
282 hostname, port, err := net.SplitHostPort(host)
283 if err != nil {
284 return host
285 }
286 if port == "" {
287 port = portMap[scheme]
288 }
289
290 return net.JoinHostPort(hostname, port)
291 }
292
293 return net.JoinHostPort(host, portMap[scheme])
294 }
295
296
297 func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) {
298 auth, err := bt.basic.Authorization()
299 if err != nil {
300 return nil, err
301 }
302
303 u, err := url.Parse(bt.realm)
304 if err != nil {
305 return nil, err
306 }
307
308 v := url.Values{}
309 v.Set("scope", strings.Join(bt.scopes, " "))
310 if bt.service != "" {
311 v.Set("service", bt.service)
312 }
313 v.Set("client_id", defaultUserAgent)
314 if auth.IdentityToken != "" {
315 v.Set("grant_type", "refresh_token")
316 v.Set("refresh_token", auth.IdentityToken)
317 } else if auth.Username != "" && auth.Password != "" {
318
319 v.Set("grant_type", "password")
320 v.Set("username", auth.Username)
321 v.Set("password", auth.Password)
322 v.Set("access_type", "offline")
323 }
324
325 client := http.Client{Transport: bt.inner}
326 req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(v.Encode()))
327 if err != nil {
328 return nil, err
329 }
330 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
331
332
333 ctx = redact.NewContext(ctx, "oauth token response contains credentials")
334
335 resp, err := client.Do(req.WithContext(ctx))
336 if err != nil {
337 return nil, err
338 }
339 defer resp.Body.Close()
340
341 if err := CheckError(resp, http.StatusOK); err != nil {
342 if bt.basic == authn.Anonymous {
343 logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
344 }
345 return nil, err
346 }
347
348 return io.ReadAll(resp.Body)
349 }
350
351
352 func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]byte, error) {
353 u, err := url.Parse(bt.realm)
354 if err != nil {
355 return nil, err
356 }
357 b := &basicTransport{
358 inner: bt.inner,
359 auth: bt.basic,
360 target: u.Host,
361 }
362 client := http.Client{Transport: b}
363
364 v := u.Query()
365 v["scope"] = bt.scopes
366 v.Set("service", bt.service)
367 u.RawQuery = v.Encode()
368
369 req, err := http.NewRequest(http.MethodGet, u.String(), nil)
370 if err != nil {
371 return nil, err
372 }
373
374
375 ctx = redact.NewContext(ctx, "basic token response contains credentials")
376
377 resp, err := client.Do(req.WithContext(ctx))
378 if err != nil {
379 return nil, err
380 }
381 defer resp.Body.Close()
382
383 if err := CheckError(resp, http.StatusOK); err != nil {
384 if bt.basic == authn.Anonymous {
385 logs.Warn.Printf("No matching credentials were found for %q", bt.registry)
386 }
387 return nil, err
388 }
389
390 return io.ReadAll(resp.Body)
391 }
392
View as plain text