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