blob: a1e98aa29333de7e69945ef21d9b8dc6860d6df3 [file] [log] [blame]
Jan Schärcc9e4d12025-04-14 10:28:40 +00001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4package registry
5
6import (
7 "context"
8 "encoding/json"
9 "fmt"
10 "net/http"
11 "net/url"
12 "strings"
13
14 "github.com/cenkalti/backoff/v4"
15)
16
17type tokenBody struct {
18 Token string `json:"token"`
19 AccessToken string `json:"access_token"`
20}
21
22// handleUnauthorized implements token authentication based on this
23// specification: https://distribution.github.io/distribution/spec/auth/token/
24//
25// The registry will return Unauthorized if a token is required and it is
26// missing or invalid (e.g. expired). We then need to ask the authorization
27// service for a token, and retry the original request with the new token.
28//
29// Some registries (e.g. Docker Hub and ghcr.io) require a token even for public
30// repositories. In this case, the authorization service returns tokens without
31// requiring any credentials.
32func (c *Client) handleUnauthorized(ctx context.Context, resp *http.Response) (retry bool, err error) {
33 // Check if we have a Bearer challenge.
34 challenges := parseAuthenticateHeader(resp.Header.Values("Www-Authenticate"))
35 var params map[string]string
36 for _, c := range challenges {
37 if strings.EqualFold(c.scheme, "bearer") {
38 params = c.params
39 break
40 }
41 }
42 realm := params["realm"]
43 if realm == "" {
44 // There is no challenge, return the original HTTP error.
45 return false, nil
46 }
47
48 // Construct token URL.
49 tokenURL, err := url.Parse(realm)
50 if err != nil {
51 return false, backoff.Permanent(fmt.Errorf("failed to parse realm: %w", err))
52 }
53 query := tokenURL.Query()
54 service := params["service"]
55 if service != "" {
56 query.Set("service", service)
57 }
58 for scope := range strings.SplitSeq(params["scope"], " ") {
59 if scope != "" {
60 query.Add("scope", scope)
61 }
62 }
63 tokenURL.RawQuery = query.Encode()
64
65 // Do token request.
66 req, err := http.NewRequestWithContext(ctx, "GET", tokenURL.String(), nil)
67 if err != nil {
68 return false, err
69 }
70 if c.UserAgent != "" {
71 req.Header.Set("User-Agent", c.UserAgent)
72 }
73 client := http.Client{Transport: c.Transport}
74 tokenResp, err := client.Do(req)
75 if err != nil {
76 return false, redactURLError(err)
77 }
78 if tokenResp.StatusCode != http.StatusOK {
79 return false, readClientError(tokenResp, req)
80 }
81 defer tokenResp.Body.Close()
82
83 // Parse token response.
84 bodyBytes, err := readFullBody(tokenResp, 1024*1024)
85 if err != nil {
86 return false, err
87 }
88 body := tokenBody{}
89 err = json.Unmarshal(bodyBytes, &body)
90 if err != nil {
91 return false, backoff.Permanent(fmt.Errorf("failed to parse token response: %w", err))
92 }
93 token := body.Token
94 if token == "" {
95 token = body.AccessToken
96 }
97 if token == "" {
98 return false, backoff.Permanent(fmt.Errorf("missing token in token response"))
99 }
100
101 c.authMu.Lock()
102 c.bearerToken = token
103 c.authMu.Unlock()
104 return true, nil
105}
106
107func (c *Client) addAuthorization(req *http.Request) {
108 c.authMu.RLock()
109 defer c.authMu.RUnlock()
110 if c.bearerToken != "" {
111 req.Header.Set("Authorization", "Bearer "+c.bearerToken)
112 }
113}