github.com/christopherobin/docker@v1.6.2/registry/auth.go (about) 1 package registry 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io/ioutil" 9 "net/http" 10 "os" 11 "path" 12 "strings" 13 "sync" 14 "time" 15 16 log "github.com/Sirupsen/logrus" 17 "github.com/docker/docker/utils" 18 ) 19 20 const ( 21 // Where we store the config file 22 CONFIGFILE = ".dockercfg" 23 ) 24 25 var ( 26 ErrConfigFileMissing = errors.New("The Auth config file is missing") 27 ) 28 29 type AuthConfig struct { 30 Username string `json:"username,omitempty"` 31 Password string `json:"password,omitempty"` 32 Auth string `json:"auth"` 33 Email string `json:"email"` 34 ServerAddress string `json:"serveraddress,omitempty"` 35 } 36 37 type ConfigFile struct { 38 Configs map[string]AuthConfig `json:"configs,omitempty"` 39 rootPath string 40 } 41 42 type RequestAuthorization struct { 43 authConfig *AuthConfig 44 registryEndpoint *Endpoint 45 resource string 46 scope string 47 actions []string 48 49 tokenLock sync.Mutex 50 tokenCache string 51 tokenExpiration time.Time 52 } 53 54 func NewRequestAuthorization(authConfig *AuthConfig, registryEndpoint *Endpoint, resource, scope string, actions []string) *RequestAuthorization { 55 return &RequestAuthorization{ 56 authConfig: authConfig, 57 registryEndpoint: registryEndpoint, 58 resource: resource, 59 scope: scope, 60 actions: actions, 61 } 62 } 63 64 func (auth *RequestAuthorization) getToken() (string, error) { 65 auth.tokenLock.Lock() 66 defer auth.tokenLock.Unlock() 67 now := time.Now() 68 if now.Before(auth.tokenExpiration) { 69 log.Debugf("Using cached token for %s", auth.authConfig.Username) 70 return auth.tokenCache, nil 71 } 72 73 client := auth.registryEndpoint.HTTPClient() 74 factory := HTTPRequestFactory(nil) 75 76 for _, challenge := range auth.registryEndpoint.AuthChallenges { 77 switch strings.ToLower(challenge.Scheme) { 78 case "basic": 79 // no token necessary 80 case "bearer": 81 log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, auth.authConfig.Username) 82 params := map[string]string{} 83 for k, v := range challenge.Parameters { 84 params[k] = v 85 } 86 params["scope"] = fmt.Sprintf("%s:%s:%s", auth.resource, auth.scope, strings.Join(auth.actions, ",")) 87 token, err := getToken(auth.authConfig.Username, auth.authConfig.Password, params, auth.registryEndpoint, client, factory) 88 if err != nil { 89 return "", err 90 } 91 auth.tokenCache = token 92 auth.tokenExpiration = now.Add(time.Minute) 93 94 return token, nil 95 default: 96 log.Infof("Unsupported auth scheme: %q", challenge.Scheme) 97 } 98 } 99 100 // Do not expire cache since there are no challenges which use a token 101 auth.tokenExpiration = time.Now().Add(time.Hour * 24) 102 103 return "", nil 104 } 105 106 func (auth *RequestAuthorization) Authorize(req *http.Request) error { 107 token, err := auth.getToken() 108 if err != nil { 109 return err 110 } 111 if token != "" { 112 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 113 } else if auth.authConfig.Username != "" && auth.authConfig.Password != "" { 114 req.SetBasicAuth(auth.authConfig.Username, auth.authConfig.Password) 115 } 116 return nil 117 } 118 119 // create a base64 encoded auth string to store in config 120 func encodeAuth(authConfig *AuthConfig) string { 121 authStr := authConfig.Username + ":" + authConfig.Password 122 msg := []byte(authStr) 123 encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) 124 base64.StdEncoding.Encode(encoded, msg) 125 return string(encoded) 126 } 127 128 // decode the auth string 129 func decodeAuth(authStr string) (string, string, error) { 130 decLen := base64.StdEncoding.DecodedLen(len(authStr)) 131 decoded := make([]byte, decLen) 132 authByte := []byte(authStr) 133 n, err := base64.StdEncoding.Decode(decoded, authByte) 134 if err != nil { 135 return "", "", err 136 } 137 if n > decLen { 138 return "", "", fmt.Errorf("Something went wrong decoding auth config") 139 } 140 arr := strings.SplitN(string(decoded), ":", 2) 141 if len(arr) != 2 { 142 return "", "", fmt.Errorf("Invalid auth configuration file") 143 } 144 password := strings.Trim(arr[1], "\x00") 145 return arr[0], password, nil 146 } 147 148 // load up the auth config information and return values 149 // FIXME: use the internal golang config parser 150 func LoadConfig(rootPath string) (*ConfigFile, error) { 151 configFile := ConfigFile{Configs: make(map[string]AuthConfig), rootPath: rootPath} 152 confFile := path.Join(rootPath, CONFIGFILE) 153 if _, err := os.Stat(confFile); err != nil { 154 return &configFile, nil //missing file is not an error 155 } 156 b, err := ioutil.ReadFile(confFile) 157 if err != nil { 158 return &configFile, err 159 } 160 161 if err := json.Unmarshal(b, &configFile.Configs); err != nil { 162 arr := strings.Split(string(b), "\n") 163 if len(arr) < 2 { 164 return &configFile, fmt.Errorf("The Auth config file is empty") 165 } 166 authConfig := AuthConfig{} 167 origAuth := strings.Split(arr[0], " = ") 168 if len(origAuth) != 2 { 169 return &configFile, fmt.Errorf("Invalid Auth config file") 170 } 171 authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1]) 172 if err != nil { 173 return &configFile, err 174 } 175 origEmail := strings.Split(arr[1], " = ") 176 if len(origEmail) != 2 { 177 return &configFile, fmt.Errorf("Invalid Auth config file") 178 } 179 authConfig.Email = origEmail[1] 180 authConfig.ServerAddress = IndexServerAddress() 181 // *TODO: Switch to using IndexServerName() instead? 182 configFile.Configs[IndexServerAddress()] = authConfig 183 } else { 184 for k, authConfig := range configFile.Configs { 185 authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth) 186 if err != nil { 187 return &configFile, err 188 } 189 authConfig.Auth = "" 190 authConfig.ServerAddress = k 191 configFile.Configs[k] = authConfig 192 } 193 } 194 return &configFile, nil 195 } 196 197 // save the auth config 198 func SaveConfig(configFile *ConfigFile) error { 199 confFile := path.Join(configFile.rootPath, CONFIGFILE) 200 if len(configFile.Configs) == 0 { 201 os.Remove(confFile) 202 return nil 203 } 204 205 configs := make(map[string]AuthConfig, len(configFile.Configs)) 206 for k, authConfig := range configFile.Configs { 207 authCopy := authConfig 208 209 authCopy.Auth = encodeAuth(&authCopy) 210 authCopy.Username = "" 211 authCopy.Password = "" 212 authCopy.ServerAddress = "" 213 configs[k] = authCopy 214 } 215 216 b, err := json.MarshalIndent(configs, "", "\t") 217 if err != nil { 218 return err 219 } 220 err = ioutil.WriteFile(confFile, b, 0600) 221 if err != nil { 222 return err 223 } 224 return nil 225 } 226 227 // Login tries to register/login to the registry server. 228 func Login(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { 229 // Separates the v2 registry login logic from the v1 logic. 230 if registryEndpoint.Version == APIVersion2 { 231 return loginV2(authConfig, registryEndpoint, factory) 232 } 233 234 return loginV1(authConfig, registryEndpoint, factory) 235 } 236 237 // loginV1 tries to register/login to the v1 registry server. 238 func loginV1(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { 239 var ( 240 status string 241 reqBody []byte 242 err error 243 client = registryEndpoint.HTTPClient() 244 reqStatusCode = 0 245 serverAddress = authConfig.ServerAddress 246 ) 247 248 log.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint) 249 250 if serverAddress == "" { 251 return "", fmt.Errorf("Server Error: Server Address not set.") 252 } 253 254 loginAgainstOfficialIndex := serverAddress == IndexServerAddress() 255 256 // to avoid sending the server address to the server it should be removed before being marshalled 257 authCopy := *authConfig 258 authCopy.ServerAddress = "" 259 260 jsonBody, err := json.Marshal(authCopy) 261 if err != nil { 262 return "", fmt.Errorf("Config Error: %s", err) 263 } 264 265 // using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status. 266 b := strings.NewReader(string(jsonBody)) 267 req1, err := client.Post(serverAddress+"users/", "application/json; charset=utf-8", b) 268 if err != nil { 269 return "", fmt.Errorf("Server Error: %s", err) 270 } 271 reqStatusCode = req1.StatusCode 272 defer req1.Body.Close() 273 reqBody, err = ioutil.ReadAll(req1.Body) 274 if err != nil { 275 return "", fmt.Errorf("Server Error: [%#v] %s", reqStatusCode, err) 276 } 277 278 if reqStatusCode == 201 { 279 if loginAgainstOfficialIndex { 280 status = "Account created. Please use the confirmation link we sent" + 281 " to your e-mail to activate it." 282 } else { 283 // *TODO: Use registry configuration to determine what this says, if anything? 284 status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it." 285 } 286 } else if reqStatusCode == 400 { 287 if string(reqBody) == "\"Username or email already exists\"" { 288 req, err := factory.NewRequest("GET", serverAddress+"users/", nil) 289 req.SetBasicAuth(authConfig.Username, authConfig.Password) 290 resp, err := client.Do(req) 291 if err != nil { 292 return "", err 293 } 294 defer resp.Body.Close() 295 body, err := ioutil.ReadAll(resp.Body) 296 if err != nil { 297 return "", err 298 } 299 if resp.StatusCode == 200 { 300 return "Login Succeeded", nil 301 } else if resp.StatusCode == 401 { 302 return "", fmt.Errorf("Wrong login/password, please try again") 303 } else if resp.StatusCode == 403 { 304 if loginAgainstOfficialIndex { 305 return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.") 306 } 307 // *TODO: Use registry configuration to determine what this says, if anything? 308 return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress) 309 } 310 return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header) 311 } 312 return "", fmt.Errorf("Registration: %s", reqBody) 313 314 } else if reqStatusCode == 401 { 315 // This case would happen with private registries where /v1/users is 316 // protected, so people can use `docker login` as an auth check. 317 req, err := factory.NewRequest("GET", serverAddress+"users/", nil) 318 req.SetBasicAuth(authConfig.Username, authConfig.Password) 319 resp, err := client.Do(req) 320 if err != nil { 321 return "", err 322 } 323 defer resp.Body.Close() 324 body, err := ioutil.ReadAll(resp.Body) 325 if err != nil { 326 return "", err 327 } 328 if resp.StatusCode == 200 { 329 return "Login Succeeded", nil 330 } else if resp.StatusCode == 401 { 331 return "", fmt.Errorf("Wrong login/password, please try again") 332 } else { 333 return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, 334 resp.StatusCode, resp.Header) 335 } 336 } else { 337 return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody) 338 } 339 return status, nil 340 } 341 342 // loginV2 tries to login to the v2 registry server. The given registry endpoint has been 343 // pinged or setup with a list of authorization challenges. Each of these challenges are 344 // tried until one of them succeeds. Currently supported challenge schemes are: 345 // HTTP Basic Authorization 346 // Token Authorization with a separate token issuing server 347 // NOTE: the v2 logic does not attempt to create a user account if one doesn't exist. For 348 // now, users should create their account through other means like directly from a web page 349 // served by the v2 registry service provider. Whether this will be supported in the future 350 // is to be determined. 351 func loginV2(authConfig *AuthConfig, registryEndpoint *Endpoint, factory *utils.HTTPRequestFactory) (string, error) { 352 log.Debugf("attempting v2 login to registry endpoint %s", registryEndpoint) 353 var ( 354 err error 355 allErrors []error 356 client = registryEndpoint.HTTPClient() 357 ) 358 359 for _, challenge := range registryEndpoint.AuthChallenges { 360 log.Debugf("trying %q auth challenge with params %s", challenge.Scheme, challenge.Parameters) 361 362 switch strings.ToLower(challenge.Scheme) { 363 case "basic": 364 err = tryV2BasicAuthLogin(authConfig, challenge.Parameters, registryEndpoint, client, factory) 365 case "bearer": 366 err = tryV2TokenAuthLogin(authConfig, challenge.Parameters, registryEndpoint, client, factory) 367 default: 368 // Unsupported challenge types are explicitly skipped. 369 err = fmt.Errorf("unsupported auth scheme: %q", challenge.Scheme) 370 } 371 372 if err == nil { 373 return "Login Succeeded", nil 374 } 375 376 log.Debugf("error trying auth challenge %q: %s", challenge.Scheme, err) 377 378 allErrors = append(allErrors, err) 379 } 380 381 return "", fmt.Errorf("no successful auth challenge for %s - errors: %s", registryEndpoint, allErrors) 382 } 383 384 func tryV2BasicAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error { 385 req, err := factory.NewRequest("GET", registryEndpoint.Path(""), nil) 386 if err != nil { 387 return err 388 } 389 390 req.SetBasicAuth(authConfig.Username, authConfig.Password) 391 392 resp, err := client.Do(req) 393 if err != nil { 394 return err 395 } 396 defer resp.Body.Close() 397 398 if resp.StatusCode != http.StatusOK { 399 return fmt.Errorf("basic auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode)) 400 } 401 402 return nil 403 } 404 405 func tryV2TokenAuthLogin(authConfig *AuthConfig, params map[string]string, registryEndpoint *Endpoint, client *http.Client, factory *utils.HTTPRequestFactory) error { 406 token, err := getToken(authConfig.Username, authConfig.Password, params, registryEndpoint, client, factory) 407 if err != nil { 408 return err 409 } 410 411 req, err := factory.NewRequest("GET", registryEndpoint.Path(""), nil) 412 if err != nil { 413 return err 414 } 415 416 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 417 418 resp, err := client.Do(req) 419 if err != nil { 420 return err 421 } 422 defer resp.Body.Close() 423 424 if resp.StatusCode != http.StatusOK { 425 return fmt.Errorf("token auth attempt to %s realm %q failed with status: %d %s", registryEndpoint, params["realm"], resp.StatusCode, http.StatusText(resp.StatusCode)) 426 } 427 428 return nil 429 } 430 431 // this method matches a auth configuration to a server address or a url 432 func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig { 433 configKey := index.GetAuthConfigKey() 434 // First try the happy case 435 if c, found := config.Configs[configKey]; found || index.Official { 436 return c 437 } 438 439 convertToHostname := func(url string) string { 440 stripped := url 441 if strings.HasPrefix(url, "http://") { 442 stripped = strings.Replace(url, "http://", "", 1) 443 } else if strings.HasPrefix(url, "https://") { 444 stripped = strings.Replace(url, "https://", "", 1) 445 } 446 447 nameParts := strings.SplitN(stripped, "/", 2) 448 449 return nameParts[0] 450 } 451 452 // Maybe they have a legacy config file, we will iterate the keys converting 453 // them to the new format and testing 454 for registry, config := range config.Configs { 455 if configKey == convertToHostname(registry) { 456 return config 457 } 458 } 459 460 // When all else fails, return an empty auth config 461 return AuthConfig{} 462 }