github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/remotes/docker/auth/fetch.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package auth 18 19 import ( 20 "context" 21 "encoding/json" 22 "net/http" 23 "net/url" 24 "strings" 25 "time" 26 27 "github.com/containerd/containerd/log" 28 remoteserrors "github.com/containerd/containerd/remotes/errors" 29 "github.com/pkg/errors" 30 "golang.org/x/net/context/ctxhttp" 31 ) 32 33 var ( 34 // ErrNoToken is returned if a request is successful but the body does not 35 // contain an authorization token. 36 ErrNoToken = errors.New("authorization server did not include a token in the response") 37 ) 38 39 // GenerateTokenOptions generates options for fetching a token based on a challenge 40 func GenerateTokenOptions(ctx context.Context, host, username, secret string, c Challenge) (TokenOptions, error) { 41 realm, ok := c.Parameters["realm"] 42 if !ok { 43 return TokenOptions{}, errors.New("no realm specified for token auth challenge") 44 } 45 46 realmURL, err := url.Parse(realm) 47 if err != nil { 48 return TokenOptions{}, errors.Wrap(err, "invalid token auth challenge realm") 49 } 50 51 to := TokenOptions{ 52 Realm: realmURL.String(), 53 Service: c.Parameters["service"], 54 Username: username, 55 Secret: secret, 56 } 57 58 scope, ok := c.Parameters["scope"] 59 if ok { 60 to.Scopes = append(to.Scopes, scope) 61 } else { 62 log.G(ctx).WithField("host", host).Debug("no scope specified for token auth challenge") 63 } 64 65 return to, nil 66 } 67 68 // TokenOptions are optios for requesting a token 69 type TokenOptions struct { 70 Realm string 71 Service string 72 Scopes []string 73 Username string 74 Secret string 75 } 76 77 // OAuthTokenResponse is response from fetching token with a OAuth POST request 78 type OAuthTokenResponse struct { 79 AccessToken string `json:"access_token"` 80 RefreshToken string `json:"refresh_token"` 81 ExpiresIn int `json:"expires_in"` 82 IssuedAt time.Time `json:"issued_at"` 83 Scope string `json:"scope"` 84 } 85 86 // FetchTokenWithOAuth fetches a token using a POST request 87 func FetchTokenWithOAuth(ctx context.Context, client *http.Client, headers http.Header, clientID string, to TokenOptions) (*OAuthTokenResponse, error) { 88 form := url.Values{} 89 if len(to.Scopes) > 0 { 90 form.Set("scope", strings.Join(to.Scopes, " ")) 91 } 92 form.Set("service", to.Service) 93 form.Set("client_id", clientID) 94 95 if to.Username == "" { 96 form.Set("grant_type", "refresh_token") 97 form.Set("refresh_token", to.Secret) 98 } else { 99 form.Set("grant_type", "password") 100 form.Set("username", to.Username) 101 form.Set("password", to.Secret) 102 } 103 104 req, err := http.NewRequest("POST", to.Realm, strings.NewReader(form.Encode())) 105 if err != nil { 106 return nil, err 107 } 108 req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") 109 if headers != nil { 110 for k, v := range headers { 111 req.Header[k] = append(req.Header[k], v...) 112 } 113 } 114 115 resp, err := ctxhttp.Do(ctx, client, req) 116 if err != nil { 117 return nil, err 118 } 119 defer resp.Body.Close() 120 121 if resp.StatusCode < 200 || resp.StatusCode >= 400 { 122 return nil, errors.WithStack(remoteserrors.NewUnexpectedStatusErr(resp)) 123 } 124 125 decoder := json.NewDecoder(resp.Body) 126 127 var tr OAuthTokenResponse 128 if err = decoder.Decode(&tr); err != nil { 129 return nil, errors.Wrap(err, "unable to decode token response") 130 } 131 132 if tr.AccessToken == "" { 133 return nil, errors.WithStack(ErrNoToken) 134 } 135 136 return &tr, nil 137 } 138 139 // FetchTokenResponse is response from fetching token with GET request 140 type FetchTokenResponse struct { 141 Token string `json:"token"` 142 AccessToken string `json:"access_token"` 143 ExpiresIn int `json:"expires_in"` 144 IssuedAt time.Time `json:"issued_at"` 145 RefreshToken string `json:"refresh_token"` 146 } 147 148 // FetchToken fetches a token using a GET request 149 func FetchToken(ctx context.Context, client *http.Client, headers http.Header, to TokenOptions) (*FetchTokenResponse, error) { 150 req, err := http.NewRequest("GET", to.Realm, nil) 151 if err != nil { 152 return nil, err 153 } 154 155 if headers != nil { 156 for k, v := range headers { 157 req.Header[k] = append(req.Header[k], v...) 158 } 159 } 160 161 reqParams := req.URL.Query() 162 163 if to.Service != "" { 164 reqParams.Add("service", to.Service) 165 } 166 167 for _, scope := range to.Scopes { 168 reqParams.Add("scope", scope) 169 } 170 171 if to.Secret != "" { 172 req.SetBasicAuth(to.Username, to.Secret) 173 } 174 175 req.URL.RawQuery = reqParams.Encode() 176 177 resp, err := ctxhttp.Do(ctx, client, req) 178 if err != nil { 179 return nil, err 180 } 181 defer resp.Body.Close() 182 183 if resp.StatusCode < 200 || resp.StatusCode >= 400 { 184 return nil, errors.WithStack(remoteserrors.NewUnexpectedStatusErr(resp)) 185 } 186 187 decoder := json.NewDecoder(resp.Body) 188 189 var tr FetchTokenResponse 190 if err = decoder.Decode(&tr); err != nil { 191 return nil, errors.Wrap(err, "unable to decode token response") 192 } 193 194 // `access_token` is equivalent to `token` and if both are specified 195 // the choice is undefined. Canonicalize `access_token` by sticking 196 // things in `token`. 197 if tr.AccessToken != "" { 198 tr.Token = tr.AccessToken 199 } 200 201 if tr.Token == "" { 202 return nil, errors.WithStack(ErrNoToken) 203 } 204 205 return &tr, nil 206 }