github.com/containerd/Containerd@v1.4.13/remotes/docker/authorizer.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 docker 18 19 import ( 20 "context" 21 "encoding/base64" 22 "encoding/json" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "net/http" 27 "net/url" 28 "strings" 29 "sync" 30 "time" 31 32 "github.com/containerd/containerd/errdefs" 33 "github.com/containerd/containerd/log" 34 "github.com/containerd/containerd/version" 35 "github.com/pkg/errors" 36 "github.com/sirupsen/logrus" 37 "golang.org/x/net/context/ctxhttp" 38 ) 39 40 type dockerAuthorizer struct { 41 credentials func(string) (string, string, error) 42 43 client *http.Client 44 header http.Header 45 mu sync.Mutex 46 47 // indexed by host name 48 handlers map[string]*authHandler 49 } 50 51 // NewAuthorizer creates a Docker authorizer using the provided function to 52 // get credentials for the token server or basic auth. 53 // Deprecated: Use NewDockerAuthorizer 54 func NewAuthorizer(client *http.Client, f func(string) (string, string, error)) Authorizer { 55 return NewDockerAuthorizer(WithAuthClient(client), WithAuthCreds(f)) 56 } 57 58 type authorizerConfig struct { 59 credentials func(string) (string, string, error) 60 client *http.Client 61 header http.Header 62 } 63 64 // AuthorizerOpt configures an authorizer 65 type AuthorizerOpt func(*authorizerConfig) 66 67 // WithAuthClient provides the HTTP client for the authorizer 68 func WithAuthClient(client *http.Client) AuthorizerOpt { 69 return func(opt *authorizerConfig) { 70 opt.client = client 71 } 72 } 73 74 // WithAuthCreds provides a credential function to the authorizer 75 func WithAuthCreds(creds func(string) (string, string, error)) AuthorizerOpt { 76 return func(opt *authorizerConfig) { 77 opt.credentials = creds 78 } 79 } 80 81 // WithAuthHeader provides HTTP headers for authorization 82 func WithAuthHeader(hdr http.Header) AuthorizerOpt { 83 return func(opt *authorizerConfig) { 84 opt.header = hdr 85 } 86 } 87 88 // NewDockerAuthorizer creates an authorizer using Docker's registry 89 // authentication spec. 90 // See https://docs.docker.com/registry/spec/auth/ 91 func NewDockerAuthorizer(opts ...AuthorizerOpt) Authorizer { 92 var ao authorizerConfig 93 for _, opt := range opts { 94 opt(&ao) 95 } 96 97 if ao.client == nil { 98 ao.client = http.DefaultClient 99 } 100 101 return &dockerAuthorizer{ 102 credentials: ao.credentials, 103 client: ao.client, 104 header: ao.header, 105 handlers: make(map[string]*authHandler), 106 } 107 } 108 109 // Authorize handles auth request. 110 func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) error { 111 // skip if there is no auth handler 112 ah := a.getAuthHandler(req.URL.Host) 113 if ah == nil { 114 return nil 115 } 116 117 auth, err := ah.authorize(ctx) 118 if err != nil { 119 return err 120 } 121 122 req.Header.Set("Authorization", auth) 123 return nil 124 } 125 126 func (a *dockerAuthorizer) getAuthHandler(host string) *authHandler { 127 a.mu.Lock() 128 defer a.mu.Unlock() 129 130 return a.handlers[host] 131 } 132 133 func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error { 134 last := responses[len(responses)-1] 135 host := last.Request.URL.Host 136 137 a.mu.Lock() 138 defer a.mu.Unlock() 139 for _, c := range parseAuthHeader(last.Header) { 140 if c.scheme == bearerAuth { 141 if err := invalidAuthorization(c, responses); err != nil { 142 delete(a.handlers, host) 143 return err 144 } 145 146 // reuse existing handler 147 // 148 // assume that one registry will return the common 149 // challenge information, including realm and service. 150 // and the resource scope is only different part 151 // which can be provided by each request. 152 if _, ok := a.handlers[host]; ok { 153 return nil 154 } 155 156 common, err := a.generateTokenOptions(ctx, host, c) 157 if err != nil { 158 return err 159 } 160 161 a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common) 162 return nil 163 } else if c.scheme == basicAuth && a.credentials != nil { 164 username, secret, err := a.credentials(host) 165 if err != nil { 166 return err 167 } 168 169 if username != "" && secret != "" { 170 common := tokenOptions{ 171 username: username, 172 secret: secret, 173 } 174 175 a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common) 176 return nil 177 } 178 } 179 } 180 return errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme") 181 } 182 183 func (a *dockerAuthorizer) generateTokenOptions(ctx context.Context, host string, c challenge) (tokenOptions, error) { 184 realm, ok := c.parameters["realm"] 185 if !ok { 186 return tokenOptions{}, errors.New("no realm specified for token auth challenge") 187 } 188 189 realmURL, err := url.Parse(realm) 190 if err != nil { 191 return tokenOptions{}, errors.Wrap(err, "invalid token auth challenge realm") 192 } 193 194 to := tokenOptions{ 195 realm: realmURL.String(), 196 service: c.parameters["service"], 197 } 198 199 scope, ok := c.parameters["scope"] 200 if ok { 201 to.scopes = append(to.scopes, scope) 202 } else { 203 log.G(ctx).WithField("host", host).Debug("no scope specified for token auth challenge") 204 } 205 206 if a.credentials != nil { 207 to.username, to.secret, err = a.credentials(host) 208 if err != nil { 209 return tokenOptions{}, err 210 } 211 } 212 return to, nil 213 } 214 215 // authResult is used to control limit rate. 216 type authResult struct { 217 sync.WaitGroup 218 token string 219 err error 220 } 221 222 // authHandler is used to handle auth request per registry server. 223 type authHandler struct { 224 sync.Mutex 225 226 header http.Header 227 228 client *http.Client 229 230 // only support basic and bearer schemes 231 scheme authenticationScheme 232 233 // common contains common challenge answer 234 common tokenOptions 235 236 // scopedTokens caches token indexed by scopes, which used in 237 // bearer auth case 238 scopedTokens map[string]*authResult 239 } 240 241 func newAuthHandler(client *http.Client, hdr http.Header, scheme authenticationScheme, opts tokenOptions) *authHandler { 242 return &authHandler{ 243 header: hdr, 244 client: client, 245 scheme: scheme, 246 common: opts, 247 scopedTokens: map[string]*authResult{}, 248 } 249 } 250 251 func (ah *authHandler) authorize(ctx context.Context) (string, error) { 252 switch ah.scheme { 253 case basicAuth: 254 return ah.doBasicAuth(ctx) 255 case bearerAuth: 256 return ah.doBearerAuth(ctx) 257 default: 258 return "", errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme") 259 } 260 } 261 262 func (ah *authHandler) doBasicAuth(ctx context.Context) (string, error) { 263 username, secret := ah.common.username, ah.common.secret 264 265 if username == "" || secret == "" { 266 return "", fmt.Errorf("failed to handle basic auth because missing username or secret") 267 } 268 269 auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + secret)) 270 return fmt.Sprintf("Basic %s", auth), nil 271 } 272 273 func (ah *authHandler) doBearerAuth(ctx context.Context) (string, error) { 274 // copy common tokenOptions 275 to := ah.common 276 277 to.scopes = GetTokenScopes(ctx, to.scopes) 278 279 // Docs: https://docs.docker.com/registry/spec/auth/scope 280 scoped := strings.Join(to.scopes, " ") 281 282 ah.Lock() 283 if r, exist := ah.scopedTokens[scoped]; exist { 284 ah.Unlock() 285 r.Wait() 286 return r.token, r.err 287 } 288 289 // only one fetch token job 290 r := new(authResult) 291 r.Add(1) 292 ah.scopedTokens[scoped] = r 293 ah.Unlock() 294 295 // fetch token for the resource scope 296 var ( 297 token string 298 err error 299 ) 300 if to.secret != "" { 301 // credential information is provided, use oauth POST endpoint 302 token, err = ah.fetchTokenWithOAuth(ctx, to) 303 err = errors.Wrap(err, "failed to fetch oauth token") 304 } else { 305 // do request anonymously 306 token, err = ah.fetchToken(ctx, to) 307 err = errors.Wrap(err, "failed to fetch anonymous token") 308 } 309 token = fmt.Sprintf("Bearer %s", token) 310 311 r.token, r.err = token, err 312 r.Done() 313 return r.token, r.err 314 } 315 316 type tokenOptions struct { 317 realm string 318 service string 319 scopes []string 320 username string 321 secret string 322 } 323 324 type postTokenResponse struct { 325 AccessToken string `json:"access_token"` 326 RefreshToken string `json:"refresh_token"` 327 ExpiresIn int `json:"expires_in"` 328 IssuedAt time.Time `json:"issued_at"` 329 Scope string `json:"scope"` 330 } 331 332 func (ah *authHandler) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) { 333 form := url.Values{} 334 if len(to.scopes) > 0 { 335 form.Set("scope", strings.Join(to.scopes, " ")) 336 } 337 form.Set("service", to.service) 338 // TODO: Allow setting client_id 339 form.Set("client_id", "containerd-client") 340 341 if to.username == "" { 342 form.Set("grant_type", "refresh_token") 343 form.Set("refresh_token", to.secret) 344 } else { 345 form.Set("grant_type", "password") 346 form.Set("username", to.username) 347 form.Set("password", to.secret) 348 } 349 350 req, err := http.NewRequest("POST", to.realm, strings.NewReader(form.Encode())) 351 if err != nil { 352 return "", err 353 } 354 req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") 355 if ah.header != nil { 356 for k, v := range ah.header { 357 req.Header[k] = append(req.Header[k], v...) 358 } 359 } 360 if len(req.Header.Get("User-Agent")) == 0 { 361 req.Header.Set("User-Agent", "containerd/"+version.Version) 362 } 363 364 resp, err := ctxhttp.Do(ctx, ah.client, req) 365 if err != nil { 366 return "", err 367 } 368 defer resp.Body.Close() 369 370 // Registries without support for POST may return 404 for POST /v2/token. 371 // As of September 2017, GCR is known to return 404. 372 // As of February 2018, JFrog Artifactory is known to return 401. 373 if (resp.StatusCode == 405 && to.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 { 374 return ah.fetchToken(ctx, to) 375 } else if resp.StatusCode < 200 || resp.StatusCode >= 400 { 376 b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB 377 log.G(ctx).WithFields(logrus.Fields{ 378 "status": resp.Status, 379 "body": string(b), 380 }).Debugf("token request failed") 381 // TODO: handle error body and write debug output 382 return "", errors.Errorf("unexpected status: %s", resp.Status) 383 } 384 385 decoder := json.NewDecoder(resp.Body) 386 387 var tr postTokenResponse 388 if err = decoder.Decode(&tr); err != nil { 389 return "", fmt.Errorf("unable to decode token response: %s", err) 390 } 391 392 return tr.AccessToken, nil 393 } 394 395 type getTokenResponse struct { 396 Token string `json:"token"` 397 AccessToken string `json:"access_token"` 398 ExpiresIn int `json:"expires_in"` 399 IssuedAt time.Time `json:"issued_at"` 400 RefreshToken string `json:"refresh_token"` 401 } 402 403 // fetchToken fetches a token using a GET request 404 func (ah *authHandler) fetchToken(ctx context.Context, to tokenOptions) (string, error) { 405 req, err := http.NewRequest("GET", to.realm, nil) 406 if err != nil { 407 return "", err 408 } 409 410 if ah.header != nil { 411 for k, v := range ah.header { 412 req.Header[k] = append(req.Header[k], v...) 413 } 414 } 415 if len(req.Header.Get("User-Agent")) == 0 { 416 req.Header.Set("User-Agent", "containerd/"+version.Version) 417 } 418 419 reqParams := req.URL.Query() 420 421 if to.service != "" { 422 reqParams.Add("service", to.service) 423 } 424 425 for _, scope := range to.scopes { 426 reqParams.Add("scope", scope) 427 } 428 429 if to.secret != "" { 430 req.SetBasicAuth(to.username, to.secret) 431 } 432 433 req.URL.RawQuery = reqParams.Encode() 434 435 resp, err := ctxhttp.Do(ctx, ah.client, req) 436 if err != nil { 437 return "", err 438 } 439 defer resp.Body.Close() 440 441 if resp.StatusCode < 200 || resp.StatusCode >= 400 { 442 // TODO: handle error body and write debug output 443 return "", errors.Errorf("unexpected status: %s", resp.Status) 444 } 445 446 decoder := json.NewDecoder(resp.Body) 447 448 var tr getTokenResponse 449 if err = decoder.Decode(&tr); err != nil { 450 return "", fmt.Errorf("unable to decode token response: %s", err) 451 } 452 453 // `access_token` is equivalent to `token` and if both are specified 454 // the choice is undefined. Canonicalize `access_token` by sticking 455 // things in `token`. 456 if tr.AccessToken != "" { 457 tr.Token = tr.AccessToken 458 } 459 460 if tr.Token == "" { 461 return "", ErrNoToken 462 } 463 464 return tr.Token, nil 465 } 466 467 func invalidAuthorization(c challenge, responses []*http.Response) error { 468 errStr := c.parameters["error"] 469 if errStr == "" { 470 return nil 471 } 472 473 n := len(responses) 474 if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) { 475 return nil 476 } 477 478 return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr) 479 } 480 481 func sameRequest(r1, r2 *http.Request) bool { 482 if r1.Method != r2.Method { 483 return false 484 } 485 if *r1.URL != *r2.URL { 486 return false 487 } 488 return true 489 }