1package auth 2 3import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/url" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/docker/distribution/registry/client" 14 "github.com/docker/distribution/registry/client/auth/challenge" 15 "github.com/docker/distribution/registry/client/transport" 16) 17 18var ( 19 // ErrNoBasicAuthCredentials is returned if a request can't be authorized with 20 // basic auth due to lack of credentials. 21 ErrNoBasicAuthCredentials = errors.New("no basic auth credentials") 22 23 // ErrNoToken is returned if a request is successful but the body does not 24 // contain an authorization token. 25 ErrNoToken = errors.New("authorization server did not include a token in the response") 26) 27 28const defaultClientID = "registry-client" 29 30// AuthenticationHandler is an interface for authorizing a request from 31// params from a "WWW-Authenicate" header for a single scheme. 32type AuthenticationHandler interface { 33 // Scheme returns the scheme as expected from the "WWW-Authenicate" header. 34 Scheme() string 35 36 // AuthorizeRequest adds the authorization header to a request (if needed) 37 // using the parameters from "WWW-Authenticate" method. The parameters 38 // values depend on the scheme. 39 AuthorizeRequest(req *http.Request, params map[string]string) error 40} 41 42// CredentialStore is an interface for getting credentials for 43// a given URL 44type CredentialStore interface { 45 // Basic returns basic auth for the given URL 46 Basic(*url.URL) (string, string) 47 48 // RefreshToken returns a refresh token for the 49 // given URL and service 50 RefreshToken(*url.URL, string) string 51 52 // SetRefreshToken sets the refresh token if none 53 // is provided for the given url and service 54 SetRefreshToken(realm *url.URL, service, token string) 55} 56 57// NewAuthorizer creates an authorizer which can handle multiple authentication 58// schemes. The handlers are tried in order, the higher priority authentication 59// methods should be first. The challengeMap holds a list of challenges for 60// a given root API endpoint (for example "https://registry-1.docker.io/v2/"). 61func NewAuthorizer(manager challenge.Manager, handlers ...AuthenticationHandler) transport.RequestModifier { 62 return &endpointAuthorizer{ 63 challenges: manager, 64 handlers: handlers, 65 } 66} 67 68type endpointAuthorizer struct { 69 challenges challenge.Manager 70 handlers []AuthenticationHandler 71 transport http.RoundTripper 72} 73 74func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error { 75 pingPath := req.URL.Path 76 if v2Root := strings.Index(req.URL.Path, "/v2/"); v2Root != -1 { 77 pingPath = pingPath[:v2Root+4] 78 } else if v1Root := strings.Index(req.URL.Path, "/v1/"); v1Root != -1 { 79 pingPath = pingPath[:v1Root] + "/v2/" 80 } else { 81 return nil 82 } 83 84 ping := url.URL{ 85 Host: req.URL.Host, 86 Scheme: req.URL.Scheme, 87 Path: pingPath, 88 } 89 90 challenges, err := ea.challenges.GetChallenges(ping) 91 if err != nil { 92 return err 93 } 94 95 if len(challenges) > 0 { 96 for _, handler := range ea.handlers { 97 for _, c := range challenges { 98 if c.Scheme != handler.Scheme() { 99 continue 100 } 101 if err := handler.AuthorizeRequest(req, c.Parameters); err != nil { 102 return err 103 } 104 } 105 } 106 } 107 108 return nil 109} 110 111// This is the minimum duration a token can last (in seconds). 112// A token must not live less than 60 seconds because older versions 113// of the Docker client didn't read their expiration from the token 114// response and assumed 60 seconds. So to remain compatible with 115// those implementations, a token must live at least this long. 116const minimumTokenLifetimeSeconds = 60 117 118// Private interface for time used by this package to enable tests to provide their own implementation. 119type clock interface { 120 Now() time.Time 121} 122 123type tokenHandler struct { 124 header http.Header 125 creds CredentialStore 126 transport http.RoundTripper 127 clock clock 128 129 offlineAccess bool 130 forceOAuth bool 131 clientID string 132 scopes []Scope 133 134 tokenLock sync.Mutex 135 tokenCache string 136 tokenExpiration time.Time 137 138 logger Logger 139} 140 141// Scope is a type which is serializable to a string 142// using the allow scope grammar. 143type Scope interface { 144 String() string 145} 146 147// RepositoryScope represents a token scope for access 148// to a repository. 149type RepositoryScope struct { 150 Repository string 151 Class string 152 Actions []string 153} 154 155// String returns the string representation of the repository 156// using the scope grammar 157func (rs RepositoryScope) String() string { 158 repoType := "repository" 159 // Keep existing format for image class to maintain backwards compatibility 160 // with authorization servers which do not support the expanded grammar. 161 if rs.Class != "" && rs.Class != "image" { 162 repoType = fmt.Sprintf("%s(%s)", repoType, rs.Class) 163 } 164 return fmt.Sprintf("%s:%s:%s", repoType, rs.Repository, strings.Join(rs.Actions, ",")) 165} 166 167// RegistryScope represents a token scope for access 168// to resources in the registry. 169type RegistryScope struct { 170 Name string 171 Actions []string 172} 173 174// String returns the string representation of the user 175// using the scope grammar 176func (rs RegistryScope) String() string { 177 return fmt.Sprintf("registry:%s:%s", rs.Name, strings.Join(rs.Actions, ",")) 178} 179 180// Logger defines the injectable logging interface, used on TokenHandlers. 181type Logger interface { 182 Debugf(format string, args ...interface{}) 183} 184 185func logDebugf(logger Logger, format string, args ...interface{}) { 186 if logger == nil { 187 return 188 } 189 logger.Debugf(format, args...) 190} 191 192// TokenHandlerOptions is used to configure a new token handler 193type TokenHandlerOptions struct { 194 Transport http.RoundTripper 195 Credentials CredentialStore 196 197 OfflineAccess bool 198 ForceOAuth bool 199 ClientID string 200 Scopes []Scope 201 Logger Logger 202} 203 204// An implementation of clock for providing real time data. 205type realClock struct{} 206 207// Now implements clock 208func (realClock) Now() time.Time { return time.Now() } 209 210// NewTokenHandler creates a new AuthenicationHandler which supports 211// fetching tokens from a remote token server. 212func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler { 213 // Create options... 214 return NewTokenHandlerWithOptions(TokenHandlerOptions{ 215 Transport: transport, 216 Credentials: creds, 217 Scopes: []Scope{ 218 RepositoryScope{ 219 Repository: scope, 220 Actions: actions, 221 }, 222 }, 223 }) 224} 225 226// NewTokenHandlerWithOptions creates a new token handler using the provided 227// options structure. 228func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler { 229 handler := &tokenHandler{ 230 transport: options.Transport, 231 creds: options.Credentials, 232 offlineAccess: options.OfflineAccess, 233 forceOAuth: options.ForceOAuth, 234 clientID: options.ClientID, 235 scopes: options.Scopes, 236 clock: realClock{}, 237 logger: options.Logger, 238 } 239 240 return handler 241} 242 243func (th *tokenHandler) client() *http.Client { 244 return &http.Client{ 245 Transport: th.transport, 246 Timeout: 15 * time.Second, 247 } 248} 249 250func (th *tokenHandler) Scheme() string { 251 return "bearer" 252} 253 254func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { 255 var additionalScopes []string 256 if fromParam := req.URL.Query().Get("from"); fromParam != "" { 257 additionalScopes = append(additionalScopes, RepositoryScope{ 258 Repository: fromParam, 259 Actions: []string{"pull"}, 260 }.String()) 261 } 262 263 token, err := th.getToken(params, additionalScopes...) 264 if err != nil { 265 return err 266 } 267 268 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 269 270 return nil 271} 272 273func (th *tokenHandler) getToken(params map[string]string, additionalScopes ...string) (string, error) { 274 th.tokenLock.Lock() 275 defer th.tokenLock.Unlock() 276 scopes := make([]string, 0, len(th.scopes)+len(additionalScopes)) 277 for _, scope := range th.scopes { 278 scopes = append(scopes, scope.String()) 279 } 280 var addedScopes bool 281 for _, scope := range additionalScopes { 282 if hasScope(scopes, scope) { 283 continue 284 } 285 scopes = append(scopes, scope) 286 addedScopes = true 287 } 288 289 now := th.clock.Now() 290 if now.After(th.tokenExpiration) || addedScopes { 291 token, expiration, err := th.fetchToken(params, scopes) 292 if err != nil { 293 return "", err 294 } 295 296 // do not update cache for added scope tokens 297 if !addedScopes { 298 th.tokenCache = token 299 th.tokenExpiration = expiration 300 } 301 302 return token, nil 303 } 304 305 return th.tokenCache, nil 306} 307 308func hasScope(scopes []string, scope string) bool { 309 for _, s := range scopes { 310 if s == scope { 311 return true 312 } 313 } 314 return false 315} 316 317type postTokenResponse struct { 318 AccessToken string `json:"access_token"` 319 RefreshToken string `json:"refresh_token"` 320 ExpiresIn int `json:"expires_in"` 321 IssuedAt time.Time `json:"issued_at"` 322 Scope string `json:"scope"` 323} 324 325func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) { 326 form := url.Values{} 327 form.Set("scope", strings.Join(scopes, " ")) 328 form.Set("service", service) 329 330 clientID := th.clientID 331 if clientID == "" { 332 // Use default client, this is a required field 333 clientID = defaultClientID 334 } 335 form.Set("client_id", clientID) 336 337 if refreshToken != "" { 338 form.Set("grant_type", "refresh_token") 339 form.Set("refresh_token", refreshToken) 340 } else if th.creds != nil { 341 form.Set("grant_type", "password") 342 username, password := th.creds.Basic(realm) 343 form.Set("username", username) 344 form.Set("password", password) 345 346 // attempt to get a refresh token 347 form.Set("access_type", "offline") 348 } else { 349 // refuse to do oauth without a grant type 350 return "", time.Time{}, fmt.Errorf("no supported grant type") 351 } 352 353 resp, err := th.client().PostForm(realm.String(), form) 354 if err != nil { 355 return "", time.Time{}, err 356 } 357 defer resp.Body.Close() 358 359 if !client.SuccessStatus(resp.StatusCode) { 360 err := client.HandleErrorResponse(resp) 361 return "", time.Time{}, err 362 } 363 364 decoder := json.NewDecoder(resp.Body) 365 366 var tr postTokenResponse 367 if err = decoder.Decode(&tr); err != nil { 368 return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err) 369 } 370 371 if tr.RefreshToken != "" && tr.RefreshToken != refreshToken { 372 th.creds.SetRefreshToken(realm, service, tr.RefreshToken) 373 } 374 375 if tr.ExpiresIn < minimumTokenLifetimeSeconds { 376 // The default/minimum lifetime. 377 tr.ExpiresIn = minimumTokenLifetimeSeconds 378 logDebugf(th.logger, "Increasing token expiration to: %d seconds", tr.ExpiresIn) 379 } 380 381 if tr.IssuedAt.IsZero() { 382 // issued_at is optional in the token response. 383 tr.IssuedAt = th.clock.Now().UTC() 384 } 385 386 return tr.AccessToken, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil 387} 388 389type getTokenResponse struct { 390 Token string `json:"token"` 391 AccessToken string `json:"access_token"` 392 ExpiresIn int `json:"expires_in"` 393 IssuedAt time.Time `json:"issued_at"` 394 RefreshToken string `json:"refresh_token"` 395} 396 397func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string, scopes []string) (token string, expiration time.Time, err error) { 398 399 req, err := http.NewRequest("GET", realm.String(), nil) 400 if err != nil { 401 return "", time.Time{}, err 402 } 403 404 reqParams := req.URL.Query() 405 406 if service != "" { 407 reqParams.Add("service", service) 408 } 409 410 for _, scope := range scopes { 411 reqParams.Add("scope", scope) 412 } 413 414 if th.offlineAccess { 415 reqParams.Add("offline_token", "true") 416 clientID := th.clientID 417 if clientID == "" { 418 clientID = defaultClientID 419 } 420 reqParams.Add("client_id", clientID) 421 } 422 423 if th.creds != nil { 424 username, password := th.creds.Basic(realm) 425 if username != "" && password != "" { 426 reqParams.Add("account", username) 427 req.SetBasicAuth(username, password) 428 } 429 } 430 431 req.URL.RawQuery = reqParams.Encode() 432 433 resp, err := th.client().Do(req) 434 if err != nil { 435 return "", time.Time{}, err 436 } 437 defer resp.Body.Close() 438 439 if !client.SuccessStatus(resp.StatusCode) { 440 err := client.HandleErrorResponse(resp) 441 return "", time.Time{}, err 442 } 443 444 decoder := json.NewDecoder(resp.Body) 445 446 var tr getTokenResponse 447 if err = decoder.Decode(&tr); err != nil { 448 return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err) 449 } 450 451 if tr.RefreshToken != "" && th.creds != nil { 452 th.creds.SetRefreshToken(realm, service, tr.RefreshToken) 453 } 454 455 // `access_token` is equivalent to `token` and if both are specified 456 // the choice is undefined. Canonicalize `access_token` by sticking 457 // things in `token`. 458 if tr.AccessToken != "" { 459 tr.Token = tr.AccessToken 460 } 461 462 if tr.Token == "" { 463 return "", time.Time{}, ErrNoToken 464 } 465 466 if tr.ExpiresIn < minimumTokenLifetimeSeconds { 467 // The default/minimum lifetime. 468 tr.ExpiresIn = minimumTokenLifetimeSeconds 469 logDebugf(th.logger, "Increasing token expiration to: %d seconds", tr.ExpiresIn) 470 } 471 472 if tr.IssuedAt.IsZero() { 473 // issued_at is optional in the token response. 474 tr.IssuedAt = th.clock.Now().UTC() 475 } 476 477 return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil 478} 479 480func (th *tokenHandler) fetchToken(params map[string]string, scopes []string) (token string, expiration time.Time, err error) { 481 realm, ok := params["realm"] 482 if !ok { 483 return "", time.Time{}, errors.New("no realm specified for token auth challenge") 484 } 485 486 // TODO(dmcgowan): Handle empty scheme and relative realm 487 realmURL, err := url.Parse(realm) 488 if err != nil { 489 return "", time.Time{}, fmt.Errorf("invalid token auth challenge realm: %s", err) 490 } 491 492 service := params["service"] 493 494 var refreshToken string 495 496 if th.creds != nil { 497 refreshToken = th.creds.RefreshToken(realmURL, service) 498 } 499 500 if refreshToken != "" || th.forceOAuth { 501 return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes) 502 } 503 504 return th.fetchTokenWithBasicAuth(realmURL, service, scopes) 505} 506 507type basicHandler struct { 508 creds CredentialStore 509} 510 511// NewBasicHandler creaters a new authentiation handler which adds 512// basic authentication credentials to a request. 513func NewBasicHandler(creds CredentialStore) AuthenticationHandler { 514 return &basicHandler{ 515 creds: creds, 516 } 517} 518 519func (*basicHandler) Scheme() string { 520 return "basic" 521} 522 523func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { 524 if bh.creds != nil { 525 username, password := bh.creds.Basic(req.URL) 526 if username != "" && password != "" { 527 req.SetBasicAuth(username, password) 528 return nil 529 } 530 } 531 return ErrNoBasicAuthCredentials 532} 533