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  }