Browse Source

Merge pull request #20970 from dmcgowan/login-oauth

OAuth support for registries
Vincent Demeester 9 năm trước cách đây
mục cha
commit
b9361f02da
30 tập tin đã thay đổi với 470 bổ sung150 xóa
  1. 1 1
      api/client/info.go
  2. 6 1
      api/client/login.go
  3. 7 0
      api/client/trust.go
  4. 1 1
      api/server/router/system/backend.go
  5. 3 2
      api/server/router/system/system_routes.go
  6. 23 7
      cliconfig/credentials/native_store.go
  7. 51 6
      cliconfig/credentials/native_store_test.go
  8. 1 1
      daemon/daemon.go
  9. 19 1
      distribution/registry.go
  10. 1 0
      docs/reference/api/docker_remote_api.md
  11. 11 6
      docs/reference/api/docker_remote_api_v1.23.md
  12. 7 4
      docs/reference/commandline/login.md
  13. 1 1
      hack/vendor.sh
  14. 2 2
      integration-cli/fixtures/auth/docker-credential-shell-test
  15. 42 23
      registry/auth.go
  16. 16 6
      registry/service.go
  17. 1 1
      registry/service_v2.go
  18. 1 1
      vendor/src/github.com/docker/distribution/CONTRIBUTING.md
  19. 1 1
      vendor/src/github.com/docker/distribution/Dockerfile
  20. 2 2
      vendor/src/github.com/docker/distribution/Makefile
  21. 1 1
      vendor/src/github.com/docker/distribution/README.md
  22. 5 0
      vendor/src/github.com/docker/distribution/blobs.go
  23. 4 0
      vendor/src/github.com/docker/distribution/errors.go
  24. 11 5
      vendor/src/github.com/docker/distribution/manifests.go
  25. 1 1
      vendor/src/github.com/docker/distribution/reference/reference.go
  26. 11 0
      vendor/src/github.com/docker/distribution/registry.go
  27. 2 2
      vendor/src/github.com/docker/distribution/registry/api/v2/descriptors.go
  28. 216 70
      vendor/src/github.com/docker/distribution/registry/client/auth/session.go
  29. 21 3
      vendor/src/github.com/docker/distribution/registry/client/repository.go
  30. 1 1
      vendor/src/github.com/docker/distribution/registry/client/transport/http_reader.go

+ 1 - 1
api/client/info.go

@@ -93,8 +93,8 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
 		u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username
 		u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username
 		if len(u) > 0 {
 		if len(u) > 0 {
 			fmt.Fprintf(cli.out, "Username: %v\n", u)
 			fmt.Fprintf(cli.out, "Username: %v\n", u)
-			fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress)
 		}
 		}
+		fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress)
 	}
 	}
 
 
 	// Only output these warnings if the server does not support these features
 	// Only output these warnings if the server does not support these features

+ 6 - 1
api/client/login.go

@@ -57,12 +57,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
 		return err
 		return err
 	}
 	}
 
 
+	if response.IdentityToken != "" {
+		authConfig.Password = ""
+		authConfig.IdentityToken = response.IdentityToken
+	}
 	if err := storeCredentials(cli.configFile, authConfig); err != nil {
 	if err := storeCredentials(cli.configFile, authConfig); err != nil {
 		return fmt.Errorf("Error saving credentials: %v", err)
 		return fmt.Errorf("Error saving credentials: %v", err)
 	}
 	}
 
 
 	if response.Status != "" {
 	if response.Status != "" {
-		fmt.Fprintf(cli.out, "%s\n", response.Status)
+		fmt.Fprintln(cli.out, response.Status)
 	}
 	}
 	return nil
 	return nil
 }
 }
@@ -120,6 +124,7 @@ func (cli *DockerCli) configureAuth(flUser, flPassword, serverAddress string, is
 	authconfig.Username = flUser
 	authconfig.Username = flUser
 	authconfig.Password = flPassword
 	authconfig.Password = flPassword
 	authconfig.ServerAddress = serverAddress
 	authconfig.ServerAddress = serverAddress
+	authconfig.IdentityToken = ""
 
 
 	return authconfig, nil
 	return authconfig, nil
 }
 }

+ 7 - 0
api/client/trust.go

@@ -107,6 +107,13 @@ func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) {
 	return scs.auth.Username, scs.auth.Password
 	return scs.auth.Username, scs.auth.Password
 }
 }
 
 
+func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string {
+	return scs.auth.IdentityToken
+}
+
+func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {
+}
+
 // getNotaryRepository returns a NotaryRepository which stores all the
 // getNotaryRepository returns a NotaryRepository which stores all the
 // information needed to operate on a notary repository.
 // information needed to operate on a notary repository.
 // It creates a HTTP transport providing authentication support.
 // It creates a HTTP transport providing authentication support.

+ 1 - 1
api/server/router/system/backend.go

@@ -13,5 +13,5 @@ type Backend interface {
 	SystemVersion() types.Version
 	SystemVersion() types.Version
 	SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{})
 	SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{})
 	UnsubscribeFromEvents(chan interface{})
 	UnsubscribeFromEvents(chan interface{})
-	AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error)
+	AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error)
 }
 }

+ 3 - 2
api/server/router/system/system_routes.go

@@ -115,11 +115,12 @@ func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *h
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	status, err := s.backend.AuthenticateToRegistry(config)
+	status, token, err := s.backend.AuthenticateToRegistry(config)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	return httputils.WriteJSON(w, http.StatusOK, &types.AuthResponse{
 	return httputils.WriteJSON(w, http.StatusOK, &types.AuthResponse{
-		Status: status,
+		Status:        status,
+		IdentityToken: token,
 	})
 	})
 }
 }

+ 23 - 7
cliconfig/credentials/native_store.go

@@ -13,7 +13,10 @@ import (
 	"github.com/docker/engine-api/types"
 	"github.com/docker/engine-api/types"
 )
 )
 
 
-const remoteCredentialsPrefix = "docker-credential-"
+const (
+	remoteCredentialsPrefix = "docker-credential-"
+	tokenUsername           = "<token>"
+)
 
 
 // Standarize the not found error, so every helper returns
 // Standarize the not found error, so every helper returns
 // the same message and docker can handle it properly.
 // the same message and docker can handle it properly.
@@ -29,14 +32,14 @@ type command interface {
 type credentialsRequest struct {
 type credentialsRequest struct {
 	ServerURL string
 	ServerURL string
 	Username  string
 	Username  string
-	Password  string
+	Secret    string
 }
 }
 
 
 // credentialsGetResponse is the information serialized from a remote store
 // credentialsGetResponse is the information serialized from a remote store
 // when the plugin sends requests to get the user credentials.
 // when the plugin sends requests to get the user credentials.
 type credentialsGetResponse struct {
 type credentialsGetResponse struct {
 	Username string
 	Username string
-	Password string
+	Secret   string
 }
 }
 
 
 // nativeStore implements a credentials store
 // nativeStore implements a credentials store
@@ -76,6 +79,7 @@ func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) {
 		return auth, err
 		return auth, err
 	}
 	}
 	auth.Username = creds.Username
 	auth.Username = creds.Username
+	auth.IdentityToken = creds.IdentityToken
 	auth.Password = creds.Password
 	auth.Password = creds.Password
 
 
 	return auth, nil
 	return auth, nil
@@ -89,6 +93,7 @@ func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) {
 		creds, _ := c.getCredentialsFromStore(s)
 		creds, _ := c.getCredentialsFromStore(s)
 		ac.Username = creds.Username
 		ac.Username = creds.Username
 		ac.Password = creds.Password
 		ac.Password = creds.Password
+		ac.IdentityToken = creds.IdentityToken
 		auths[s] = ac
 		auths[s] = ac
 	}
 	}
 
 
@@ -102,6 +107,7 @@ func (c *nativeStore) Store(authConfig types.AuthConfig) error {
 	}
 	}
 	authConfig.Username = ""
 	authConfig.Username = ""
 	authConfig.Password = ""
 	authConfig.Password = ""
+	authConfig.IdentityToken = ""
 
 
 	// Fallback to old credential in plain text to save only the email
 	// Fallback to old credential in plain text to save only the email
 	return c.fileStore.Store(authConfig)
 	return c.fileStore.Store(authConfig)
@@ -113,7 +119,12 @@ func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
 	creds := &credentialsRequest{
 	creds := &credentialsRequest{
 		ServerURL: config.ServerAddress,
 		ServerURL: config.ServerAddress,
 		Username:  config.Username,
 		Username:  config.Username,
-		Password:  config.Password,
+		Secret:    config.Password,
+	}
+
+	if config.IdentityToken != "" {
+		creds.Username = tokenUsername
+		creds.Secret = config.IdentityToken
 	}
 	}
 
 
 	buffer := new(bytes.Buffer)
 	buffer := new(bytes.Buffer)
@@ -158,13 +169,18 @@ func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthC
 		return ret, err
 		return ret, err
 	}
 	}
 
 
-	ret.Username = resp.Username
-	ret.Password = resp.Password
+	if resp.Username == tokenUsername {
+		ret.IdentityToken = resp.Secret
+	} else {
+		ret.Password = resp.Secret
+		ret.Username = resp.Username
+	}
+
 	ret.ServerAddress = serverAddress
 	ret.ServerAddress = serverAddress
 	return ret, nil
 	return ret, nil
 }
 }
 
 
-// eraseCredentialsFromStore executes the command to remove the server redentails from the native store.
+// eraseCredentialsFromStore executes the command to remove the server credentails from the native store.
 func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error {
 func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error {
 	cmd := c.commandFn("erase")
 	cmd := c.commandFn("erase")
 	cmd.Input(strings.NewReader(serverURL))
 	cmd.Input(strings.NewReader(serverURL))

+ 51 - 6
cliconfig/credentials/native_store_test.go

@@ -47,8 +47,10 @@ func (m *mockCommand) Output() ([]byte, error) {
 		}
 		}
 	case "get":
 	case "get":
 		switch inS {
 		switch inS {
-		case validServerAddress, validServerAddress2:
-			return []byte(`{"Username": "foo", "Password": "bar"}`), nil
+		case validServerAddress:
+			return []byte(`{"Username": "foo", "Secret": "bar"}`), nil
+		case validServerAddress2:
+			return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
 		case missingCredsAddress:
 		case missingCredsAddress:
 			return []byte(errCredentialsNotFound.Error()), errCommandExited
 			return []byte(errCredentialsNotFound.Error()), errCommandExited
 		case invalidServerAddress:
 		case invalidServerAddress:
@@ -118,6 +120,9 @@ func TestNativeStoreAddCredentials(t *testing.T) {
 	if a.Password != "" {
 	if a.Password != "" {
 		t.Fatalf("expected password to be empty, got %s", a.Password)
 		t.Fatalf("expected password to be empty, got %s", a.Password)
 	}
 	}
+	if a.IdentityToken != "" {
+		t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
+	}
 	if a.Email != "foo@example.com" {
 	if a.Email != "foo@example.com" {
 		t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
 		t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
 	}
 	}
@@ -174,11 +179,45 @@ func TestNativeStoreGet(t *testing.T) {
 	if a.Password != "bar" {
 	if a.Password != "bar" {
 		t.Fatalf("expected password `bar`, got %s", a.Password)
 		t.Fatalf("expected password `bar`, got %s", a.Password)
 	}
 	}
+	if a.IdentityToken != "" {
+		t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
+	}
 	if a.Email != "foo@example.com" {
 	if a.Email != "foo@example.com" {
 		t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
 		t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
 	}
 	}
 }
 }
 
 
+func TestNativeStoreGetIdentityToken(t *testing.T) {
+	f := newConfigFile(map[string]types.AuthConfig{
+		validServerAddress2: {
+			Email: "foo@example2.com",
+		},
+	})
+	f.CredentialsStore = "mock"
+
+	s := &nativeStore{
+		commandFn: mockCommandFn,
+		fileStore: NewFileStore(f),
+	}
+	a, err := s.Get(validServerAddress2)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if a.Username != "" {
+		t.Fatalf("expected username to be empty, got %s", a.Username)
+	}
+	if a.Password != "" {
+		t.Fatalf("expected password to be empty, got %s", a.Password)
+	}
+	if a.IdentityToken != "abcd1234" {
+		t.Fatalf("expected identity token `abcd1234`, got %s", a.IdentityToken)
+	}
+	if a.Email != "foo@example2.com" {
+		t.Fatalf("expected email `foo@example2.com`, got %s", a.Email)
+	}
+}
+
 func TestNativeStoreGetAll(t *testing.T) {
 func TestNativeStoreGetAll(t *testing.T) {
 	f := newConfigFile(map[string]types.AuthConfig{
 	f := newConfigFile(map[string]types.AuthConfig{
 		validServerAddress: {
 		validServerAddress: {
@@ -209,14 +248,20 @@ func TestNativeStoreGetAll(t *testing.T) {
 	if as[validServerAddress].Password != "bar" {
 	if as[validServerAddress].Password != "bar" {
 		t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password)
 		t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password)
 	}
 	}
+	if as[validServerAddress].IdentityToken != "" {
+		t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken)
+	}
 	if as[validServerAddress].Email != "foo@example.com" {
 	if as[validServerAddress].Email != "foo@example.com" {
 		t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email)
 		t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email)
 	}
 	}
-	if as[validServerAddress2].Username != "foo" {
-		t.Fatalf("expected username `foo` for %s, got %s", validServerAddress2, as[validServerAddress2].Username)
+	if as[validServerAddress2].Username != "" {
+		t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username)
+	}
+	if as[validServerAddress2].Password != "" {
+		t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password)
 	}
 	}
-	if as[validServerAddress2].Password != "bar" {
-		t.Fatalf("expected password `bar` for %s, got %s", validServerAddress2, as[validServerAddress2].Password)
+	if as[validServerAddress2].IdentityToken != "abcd1234" {
+		t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken)
 	}
 	}
 	if as[validServerAddress2].Email != "foo@example2.com" {
 	if as[validServerAddress2].Email != "foo@example2.com" {
 		t.Fatalf("expected email `foo@example2.com` for %s, got %s", validServerAddress2, as[validServerAddress2].Email)
 		t.Fatalf("expected email `foo@example2.com` for %s, got %s", validServerAddress2, as[validServerAddress2].Email)

+ 1 - 1
daemon/daemon.go

@@ -1519,7 +1519,7 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore,
 }
 }
 
 
 // AuthenticateToRegistry checks the validity of credentials in authConfig
 // AuthenticateToRegistry checks the validity of credentials in authConfig
-func (daemon *Daemon) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error) {
+func (daemon *Daemon) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error) {
 	return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent())
 	return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent())
 }
 }
 
 

+ 19 - 1
distribution/registry.go

@@ -26,6 +26,13 @@ func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) {
 	return dcs.auth.Username, dcs.auth.Password
 	return dcs.auth.Username, dcs.auth.Password
 }
 }
 
 
+func (dcs dumbCredentialStore) RefreshToken(*url.URL, string) string {
+	return dcs.auth.IdentityToken
+}
+
+func (dcs dumbCredentialStore) SetRefreshToken(*url.URL, string, string) {
+}
+
 // NewV2Repository returns a repository (v2 only). It creates a HTTP transport
 // NewV2Repository returns a repository (v2 only). It creates a HTTP transport
 // providing timeout settings and authentication support, and also verifies the
 // providing timeout settings and authentication support, and also verifies the
 // remote API version.
 // remote API version.
@@ -72,7 +79,18 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
 		modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
 		modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
 	} else {
 	} else {
 		creds := dumbCredentialStore{auth: authConfig}
 		creds := dumbCredentialStore{auth: authConfig}
-		tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...)
+		tokenHandlerOptions := auth.TokenHandlerOptions{
+			Transport:   authTransport,
+			Credentials: creds,
+			Scopes: []auth.Scope{
+				auth.RepositoryScope{
+					Repository: repoName,
+					Actions:    actions,
+				},
+			},
+			ClientID: registry.AuthClientID,
+		}
+		tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
 		basicHandler := auth.NewBasicHandler(creds)
 		basicHandler := auth.NewBasicHandler(creds)
 		modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
 		modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
 	}
 	}

+ 1 - 0
docs/reference/api/docker_remote_api.md

@@ -125,6 +125,7 @@ This section lists each version from latest to oldest.  Each listing includes a
 * `GET /info` now returns `KernelMemory` field, showing if "kernel memory limit" is supported.
 * `GET /info` now returns `KernelMemory` field, showing if "kernel memory limit" is supported.
 * `POST /containers/create` now takes `PidsLimit` field, if the kernel is >= 4.3 and the pids cgroup is supported.
 * `POST /containers/create` now takes `PidsLimit` field, if the kernel is >= 4.3 and the pids cgroup is supported.
 * `GET /containers/(id or name)/stats` now returns `pids_stats`, if the kernel is >= 4.3 and the pids cgroup is supported.
 * `GET /containers/(id or name)/stats` now returns `pids_stats`, if the kernel is >= 4.3 and the pids cgroup is supported.
+* `POST /auth` now returns an `IdentityToken` when supported by a registry.
 
 
 ### v1.22 API changes
 ### v1.22 API changes
 
 

+ 11 - 6
docs/reference/api/docker_remote_api_v1.23.md

@@ -1985,11 +1985,11 @@ Request Headers:
     }
     }
         ```
         ```
 
 
-    - Token based login:
+    - Identity token based login:
 
 
         ```
         ```
     {
     {
-            "registrytoken": "9cbaf023786cd7..."
+            "identitytoken": "9cbaf023786cd7..."
     }
     }
         ```
         ```
 
 
@@ -2119,7 +2119,8 @@ Status Codes:
 
 
 `POST /auth`
 `POST /auth`
 
 
-Get the default username and email
+Validate credentials for a registry and get identity token,
+if available, for accessing the registry without password.
 
 
 **Example request**:
 **Example request**:
 
 
@@ -2127,9 +2128,8 @@ Get the default username and email
     Content-Type: application/json
     Content-Type: application/json
 
 
     {
     {
-         "username":" hannibal",
-         "password: "xxxx",
-         "email": "hannibal@a-team.com",
+         "username": "hannibal",
+         "password": "xxxx",
          "serveraddress": "https://index.docker.io/v1/"
          "serveraddress": "https://index.docker.io/v1/"
     }
     }
 
 
@@ -2137,6 +2137,11 @@ Get the default username and email
 
 
     HTTP/1.1 200 OK
     HTTP/1.1 200 OK
 
 
+    {
+         "Status": "Login Succeeded",
+         "IdentityToken": "9cbaf023786cd7..."
+    }
+
 Status Codes:
 Status Codes:
 
 
 -   **200** – no error
 -   **200** – no error

+ 7 - 4
docs/reference/commandline/login.md

@@ -78,17 +78,20 @@ The helpers always use the first argument in the command to identify the action.
 There are only three possible values for that argument: `store`, `get`, and `erase`.
 There are only three possible values for that argument: `store`, `get`, and `erase`.
 
 
 The `store` command takes a JSON payload from the standard input. That payload carries
 The `store` command takes a JSON payload from the standard input. That payload carries
-the server address, to identify the credential, the user name and the password.
-This is an example of that payload:
+the server address, to identify the credential, the user name, and either a password
+or an identity token.
 
 
 ```json
 ```json
 {
 {
 	"ServerURL": "https://index.docker.io/v1",
 	"ServerURL": "https://index.docker.io/v1",
 	"Username": "david",
 	"Username": "david",
-	"Password": "passw0rd1"
+	"Secret": "passw0rd1"
 }
 }
 ```
 ```
 
 
+If the secret being stored is an identity token, the Username should be set to
+`<token>`.
+
 The `store` command can write error messages to `STDOUT` that the docker engine
 The `store` command can write error messages to `STDOUT` that the docker engine
 will show if there was an issue.
 will show if there was an issue.
 
 
@@ -102,7 +105,7 @@ and password from this payload:
 ```json
 ```json
 {
 {
 	"Username": "david",
 	"Username": "david",
-	"Password": "passw0rd1"
+	"Secret": "passw0rd1"
 }
 }
 ```
 ```
 
 

+ 1 - 1
hack/vendor.sh

@@ -48,7 +48,7 @@ clone git github.com/boltdb/bolt v1.1.0
 clone git github.com/miekg/dns 75e6e86cc601825c5dbcd4e0c209eab180997cd7
 clone git github.com/miekg/dns 75e6e86cc601825c5dbcd4e0c209eab180997cd7
 
 
 # get graph and distribution packages
 # get graph and distribution packages
-clone git github.com/docker/distribution 7b66c50bb7e0e4b3b83f8fd134a9f6ea4be08b57
+clone git github.com/docker/distribution db17a23b961978730892e12a0c6051d43a31aab3
 clone git github.com/vbatts/tar-split v0.9.11
 clone git github.com/vbatts/tar-split v0.9.11
 
 
 # get desired notary commit, might also need to be updated in Dockerfile
 # get desired notary commit, might also need to be updated in Dockerfile

+ 2 - 2
integration-cli/fixtures/auth/docker-credential-shell-test

@@ -8,8 +8,8 @@ case $1 in
 		server=$(echo "$in" | jq --raw-output ".ServerURL" | sha1sum - | awk '{print $1}')
 		server=$(echo "$in" | jq --raw-output ".ServerURL" | sha1sum - | awk '{print $1}')
 
 
 		username=$(echo "$in" | jq --raw-output ".Username")
 		username=$(echo "$in" | jq --raw-output ".Username")
-		password=$(echo "$in" | jq --raw-output ".Password")
-		echo "{ \"Username\": \"${username}\", \"Password\": \"${password}\" }" > $TEMP/$server
+		password=$(echo "$in" | jq --raw-output ".Secret")
+		echo "{ \"Username\": \"${username}\", \"Secret\": \"${password}\" }" > $TEMP/$server
 		;;
 		;;
 	"get")
 	"get")
 		in=$(</dev/stdin)
 		in=$(</dev/stdin)

+ 42 - 23
registry/auth.go

@@ -15,11 +15,16 @@ import (
 	registrytypes "github.com/docker/engine-api/types/registry"
 	registrytypes "github.com/docker/engine-api/types/registry"
 )
 )
 
 
+const (
+	// AuthClientID is used the ClientID used for the token server
+	AuthClientID = "docker"
+)
+
 // loginV1 tries to register/login to the v1 registry server.
 // loginV1 tries to register/login to the v1 registry server.
-func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, error) {
+func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, string, error) {
 	registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil)
 	registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil)
 	if err != nil {
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	}
 
 
 	serverAddress := registryEndpoint.String()
 	serverAddress := registryEndpoint.String()
@@ -27,48 +32,47 @@ func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent st
 	logrus.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint)
 	logrus.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint)
 
 
 	if serverAddress == "" {
 	if serverAddress == "" {
-		return "", fmt.Errorf("Server Error: Server Address not set.")
+		return "", "", fmt.Errorf("Server Error: Server Address not set.")
 	}
 	}
 
 
 	loginAgainstOfficialIndex := serverAddress == IndexServer
 	loginAgainstOfficialIndex := serverAddress == IndexServer
 
 
 	req, err := http.NewRequest("GET", serverAddress+"users/", nil)
 	req, err := http.NewRequest("GET", serverAddress+"users/", nil)
 	if err != nil {
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	}
 	req.SetBasicAuth(authConfig.Username, authConfig.Password)
 	req.SetBasicAuth(authConfig.Username, authConfig.Password)
 	resp, err := registryEndpoint.client.Do(req)
 	resp, err := registryEndpoint.client.Do(req)
 	if err != nil {
 	if err != nil {
 		// fallback when request could not be completed
 		// fallback when request could not be completed
-		return "", fallbackError{
+		return "", "", fallbackError{
 			err: err,
 			err: err,
 		}
 		}
 	}
 	}
 	defer resp.Body.Close()
 	defer resp.Body.Close()
 	body, err := ioutil.ReadAll(resp.Body)
 	body, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	}
 	if resp.StatusCode == http.StatusOK {
 	if resp.StatusCode == http.StatusOK {
-		return "Login Succeeded", nil
+		return "Login Succeeded", "", nil
 	} else if resp.StatusCode == http.StatusUnauthorized {
 	} else if resp.StatusCode == http.StatusUnauthorized {
 		if loginAgainstOfficialIndex {
 		if loginAgainstOfficialIndex {
-			return "", fmt.Errorf("Wrong login/password, please try again. Haven't got a Docker ID? Create one at https://hub.docker.com")
+			return "", "", fmt.Errorf("Wrong login/password, please try again. Haven't got a Docker ID? Create one at https://hub.docker.com")
 		}
 		}
-		return "", fmt.Errorf("Wrong login/password, please try again")
+		return "", "", fmt.Errorf("Wrong login/password, please try again")
 	} else if resp.StatusCode == http.StatusForbidden {
 	} else if resp.StatusCode == http.StatusForbidden {
 		if loginAgainstOfficialIndex {
 		if loginAgainstOfficialIndex {
-			return "", fmt.Errorf("Login: Account is not active. Please check your e-mail for a confirmation link.")
+			return "", "", fmt.Errorf("Login: Account is not active. Please check your e-mail for a confirmation link.")
 		}
 		}
 		// *TODO: Use registry configuration to determine what this says, if anything?
 		// *TODO: Use registry configuration to determine what this says, if anything?
-		return "", fmt.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
+		return "", "", fmt.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
 	} else if resp.StatusCode == http.StatusInternalServerError { // Issue #14326
 	} else if resp.StatusCode == http.StatusInternalServerError { // Issue #14326
 		logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body)
 		logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body)
-		return "", fmt.Errorf("Internal Server Error")
-	} else {
-		return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
-			resp.StatusCode, resp.Header)
+		return "", "", fmt.Errorf("Internal Server Error")
 	}
 	}
+	return "", "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
+		resp.StatusCode, resp.Header)
 }
 }
 
 
 type loginCredentialStore struct {
 type loginCredentialStore struct {
@@ -79,6 +83,14 @@ func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
 	return lcs.authConfig.Username, lcs.authConfig.Password
 	return lcs.authConfig.Username, lcs.authConfig.Password
 }
 }
 
 
+func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string {
+	return lcs.authConfig.IdentityToken
+}
+
+func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) {
+	lcs.authConfig.IdentityToken = token
+}
+
 type fallbackError struct {
 type fallbackError struct {
 	err error
 	err error
 }
 }
@@ -90,7 +102,7 @@ func (err fallbackError) Error() string {
 // loginV2 tries to login to the v2 registry server. The given registry
 // loginV2 tries to login to the v2 registry server. The given registry
 // endpoint will be pinged to get authorization challenges. These challenges
 // endpoint will be pinged to get authorization challenges. These challenges
 // will be used to authenticate against the registry to validate credentials.
 // will be used to authenticate against the registry to validate credentials.
-func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, error) {
+func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, string, error) {
 	logrus.Debugf("attempting v2 login to registry endpoint %s", endpoint)
 	logrus.Debugf("attempting v2 login to registry endpoint %s", endpoint)
 
 
 	modifiers := DockerHeaders(userAgent, nil)
 	modifiers := DockerHeaders(userAgent, nil)
@@ -101,14 +113,21 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
 		if !foundV2 {
 		if !foundV2 {
 			err = fallbackError{err: err}
 			err = fallbackError{err: err}
 		}
 		}
-		return "", err
+		return "", "", err
 	}
 	}
 
 
+	credentialAuthConfig := *authConfig
 	creds := loginCredentialStore{
 	creds := loginCredentialStore{
-		authConfig: authConfig,
+		authConfig: &credentialAuthConfig,
 	}
 	}
 
 
-	tokenHandler := auth.NewTokenHandler(authTransport, creds, "")
+	tokenHandlerOptions := auth.TokenHandlerOptions{
+		Transport:     authTransport,
+		Credentials:   creds,
+		OfflineAccess: true,
+		ClientID:      AuthClientID,
+	}
+	tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
 	basicHandler := auth.NewBasicHandler(creds)
 	basicHandler := auth.NewBasicHandler(creds)
 	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
 	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
 	tr := transport.NewTransport(authTransport, modifiers...)
 	tr := transport.NewTransport(authTransport, modifiers...)
@@ -124,7 +143,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
 		if !foundV2 {
 		if !foundV2 {
 			err = fallbackError{err: err}
 			err = fallbackError{err: err}
 		}
 		}
-		return "", err
+		return "", "", err
 	}
 	}
 
 
 	resp, err := loginClient.Do(req)
 	resp, err := loginClient.Do(req)
@@ -132,7 +151,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
 		if !foundV2 {
 		if !foundV2 {
 			err = fallbackError{err: err}
 			err = fallbackError{err: err}
 		}
 		}
-		return "", err
+		return "", "", err
 	}
 	}
 	defer resp.Body.Close()
 	defer resp.Body.Close()
 
 
@@ -142,10 +161,10 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
 		if !foundV2 {
 		if !foundV2 {
 			err = fallbackError{err: err}
 			err = fallbackError{err: err}
 		}
 		}
-		return "", err
+		return "", "", err
 	}
 	}
 
 
-	return "Login Succeeded", nil
+	return "Login Succeeded", credentialAuthConfig.IdentityToken, nil
 
 
 }
 }
 
 

+ 16 - 6
registry/service.go

@@ -2,6 +2,7 @@ package registry
 
 
 import (
 import (
 	"crypto/tls"
 	"crypto/tls"
+	"fmt"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"strings"
 	"strings"
@@ -34,10 +35,19 @@ func (s *Service) ServiceConfig() *registrytypes.ServiceConfig {
 // Auth contacts the public registry with the provided credentials,
 // Auth contacts the public registry with the provided credentials,
 // and returns OK if authentication was successful.
 // and returns OK if authentication was successful.
 // It can be used to verify the validity of a client's credentials.
 // It can be used to verify the validity of a client's credentials.
-func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status string, err error) {
-	endpoints, err := s.LookupPushEndpoints(authConfig.ServerAddress)
+func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status, token string, err error) {
+	serverAddress := authConfig.ServerAddress
+	if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
+		serverAddress = "https://" + serverAddress
+	}
+	u, err := url.Parse(serverAddress)
+	if err != nil {
+		return "", "", fmt.Errorf("unable to parse server address: %v", err)
+	}
+
+	endpoints, err := s.LookupPushEndpoints(u.Host)
 	if err != nil {
 	if err != nil {
-		return "", err
+		return "", "", err
 	}
 	}
 
 
 	for _, endpoint := range endpoints {
 	for _, endpoint := range endpoints {
@@ -46,7 +56,7 @@ func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status s
 			login = loginV1
 			login = loginV1
 		}
 		}
 
 
-		status, err = login(authConfig, endpoint, userAgent)
+		status, token, err = login(authConfig, endpoint, userAgent)
 		if err == nil {
 		if err == nil {
 			return
 			return
 		}
 		}
@@ -55,10 +65,10 @@ func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status s
 			logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err)
 			logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err)
 			continue
 			continue
 		}
 		}
-		return "", err
+		return "", "", err
 	}
 	}
 
 
-	return "", err
+	return "", "", err
 }
 }
 
 
 // splitReposSearchTerm breaks a search term into an index name and remote name
 // splitReposSearchTerm breaks a search term into an index name and remote name

+ 1 - 1
registry/service_v2.go

@@ -10,7 +10,7 @@ import (
 func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
 func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
 	var cfg = tlsconfig.ServerDefault
 	var cfg = tlsconfig.ServerDefault
 	tlsConfig := &cfg
 	tlsConfig := &cfg
-	if hostname == DefaultNamespace {
+	if hostname == DefaultNamespace || hostname == DefaultV1Registry.Host {
 		// v2 mirrors
 		// v2 mirrors
 		for _, mirror := range s.config.Mirrors {
 		for _, mirror := range s.config.Mirrors {
 			if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
 			if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {

+ 1 - 1
vendor/src/github.com/docker/distribution/CONTRIBUTING.md

@@ -90,7 +90,7 @@ It's mandatory to:
 
 
 Complying to these simple rules will greatly accelerate the review process, and will ensure you have a pleasant experience in contributing code to the Registry.
 Complying to these simple rules will greatly accelerate the review process, and will ensure you have a pleasant experience in contributing code to the Registry.
 
 
-Have a look at a great, succesful contribution: the [Ceph driver PR](https://github.com/docker/distribution/pull/443)
+Have a look at a great, successful contribution: the [Ceph driver PR](https://github.com/docker/distribution/pull/443)
 
 
 ## Coding Style
 ## Coding Style
 
 

+ 1 - 1
vendor/src/github.com/docker/distribution/Dockerfile

@@ -16,4 +16,4 @@ RUN make PREFIX=/go clean binaries
 VOLUME ["/var/lib/registry"]
 VOLUME ["/var/lib/registry"]
 EXPOSE 5000
 EXPOSE 5000
 ENTRYPOINT ["registry"]
 ENTRYPOINT ["registry"]
-CMD ["/etc/docker/registry/config.yml"]
+CMD ["serve", "/etc/docker/registry/config.yml"]

+ 2 - 2
vendor/src/github.com/docker/distribution/Makefile

@@ -14,8 +14,8 @@ endif
 GO_LDFLAGS=-ldflags "-X `go list ./version`.Version=$(VERSION)"
 GO_LDFLAGS=-ldflags "-X `go list ./version`.Version=$(VERSION)"
 
 
 .PHONY: clean all fmt vet lint build test binaries
 .PHONY: clean all fmt vet lint build test binaries
-.DEFAULT: default
-all: AUTHORS clean fmt vet fmt lint build test binaries
+.DEFAULT: all
+all: fmt vet fmt lint build test binaries
 
 
 AUTHORS: .mailmap .git/HEAD
 AUTHORS: .mailmap .git/HEAD
 	 git log --format='%aN <%aE>' | sort -fu > $@
 	 git log --format='%aN <%aE>' | sort -fu > $@

+ 1 - 1
vendor/src/github.com/docker/distribution/README.md

@@ -128,4 +128,4 @@ avenues are available for support:
 
 
 ## License
 ## License
 
 
-This project is distributed under [Apache License, Version 2.0](LICENSE.md).
+This project is distributed under [Apache License, Version 2.0](LICENSE).

+ 5 - 0
vendor/src/github.com/docker/distribution/blobs.go

@@ -97,6 +97,11 @@ type BlobDeleter interface {
 	Delete(ctx context.Context, dgst digest.Digest) error
 	Delete(ctx context.Context, dgst digest.Digest) error
 }
 }
 
 
+// BlobEnumerator enables iterating over blobs from storage
+type BlobEnumerator interface {
+	Enumerate(ctx context.Context, ingester func(dgst digest.Digest) error) error
+}
+
 // BlobDescriptorService manages metadata about a blob by digest. Most
 // BlobDescriptorService manages metadata about a blob by digest. Most
 // implementations will not expose such an interface explicitly. Such mappings
 // implementations will not expose such an interface explicitly. Such mappings
 // should be maintained by interacting with the BlobIngester. Hence, this is
 // should be maintained by interacting with the BlobIngester. Hence, this is

+ 4 - 0
vendor/src/github.com/docker/distribution/errors.go

@@ -8,6 +8,10 @@ import (
 	"github.com/docker/distribution/digest"
 	"github.com/docker/distribution/digest"
 )
 )
 
 
+// ErrAccessDenied is returned when an access to a requested resource is
+// denied.
+var ErrAccessDenied = errors.New("access denied")
+
 // ErrManifestNotModified is returned when a conditional manifest GetByTag
 // ErrManifestNotModified is returned when a conditional manifest GetByTag
 // returns nil due to the client indicating it has the latest version
 // returns nil due to the client indicating it has the latest version
 var ErrManifestNotModified = errors.New("manifest not modified")
 var ErrManifestNotModified = errors.New("manifest not modified")

+ 11 - 5
vendor/src/github.com/docker/distribution/manifests.go

@@ -53,12 +53,18 @@ type ManifestService interface {
 	// Delete removes the manifest specified by the given digest. Deleting
 	// Delete removes the manifest specified by the given digest. Deleting
 	// a manifest that doesn't exist will return ErrManifestNotFound
 	// a manifest that doesn't exist will return ErrManifestNotFound
 	Delete(ctx context.Context, dgst digest.Digest) error
 	Delete(ctx context.Context, dgst digest.Digest) error
+}
+
+// ManifestEnumerator enables iterating over manifests
+type ManifestEnumerator interface {
+	// Enumerate calls ingester for each manifest.
+	Enumerate(ctx context.Context, ingester func(digest.Digest) error) error
+}
 
 
-	// Enumerate fills 'manifests' with the manifests in this service up
-	// to the size of 'manifests' and returns 'n' for the number of entries
-	// which were filled.  'last' contains an offset in the manifest set
-	// and can be used to resume iteration.
-	//Enumerate(ctx context.Context, manifests []Manifest, last Manifest) (n int, err error)
+// SignaturesGetter provides an interface for getting the signatures of a schema1 manifest. If the digest
+// referred to is not a schema1 manifest, an error should be returned.
+type SignaturesGetter interface {
+	GetSignatures(ctx context.Context, manifestDigest digest.Digest) ([]digest.Digest, error)
 }
 }
 
 
 // Describable is an interface for descriptors
 // Describable is an interface for descriptors

+ 1 - 1
vendor/src/github.com/docker/distribution/reference/reference.go

@@ -3,7 +3,7 @@
 //
 //
 // Grammar
 // Grammar
 //
 //
-// 	reference                       := repository [ ":" tag ] [ "@" digest ]
+// 	reference                       := name [ ":" tag ] [ "@" digest ]
 //	name                            := [hostname '/'] component ['/' component]*
 //	name                            := [hostname '/'] component ['/' component]*
 //	hostname                        := hostcomponent ['.' hostcomponent]* [':' port-number]
 //	hostname                        := hostcomponent ['.' hostcomponent]* [':' port-number]
 //	hostcomponent                   := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
 //	hostcomponent                   := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/

+ 11 - 0
vendor/src/github.com/docker/distribution/registry.go

@@ -40,6 +40,17 @@ type Namespace interface {
 	// which were filled.  'last' contains an offset in the catalog, and 'err' will be
 	// which were filled.  'last' contains an offset in the catalog, and 'err' will be
 	// set to io.EOF if there are no more entries to obtain.
 	// set to io.EOF if there are no more entries to obtain.
 	Repositories(ctx context.Context, repos []string, last string) (n int, err error)
 	Repositories(ctx context.Context, repos []string, last string) (n int, err error)
+
+	// Blobs returns a blob enumerator to access all blobs
+	Blobs() BlobEnumerator
+
+	// BlobStatter returns a BlobStatter to control
+	BlobStatter() BlobStatter
+}
+
+// RepositoryEnumerator describes an operation to enumerate repositories
+type RepositoryEnumerator interface {
+	Enumerate(ctx context.Context, ingester func(string) error) error
 }
 }
 
 
 // ManifestServiceOption is a function argument for Manifest Service methods
 // ManifestServiceOption is a function argument for Manifest Service methods

+ 2 - 2
vendor/src/github.com/docker/distribution/registry/api/v2/descriptors.go

@@ -514,7 +514,7 @@ var routeDescriptors = []RouteDescriptor{
 									digestHeader,
 									digestHeader,
 								},
 								},
 								Body: BodyDescriptor{
 								Body: BodyDescriptor{
-									ContentType: "application/json; charset=utf-8",
+									ContentType: "<media type of manifest>",
 									Format:      manifestBody,
 									Format:      manifestBody,
 								},
 								},
 							},
 							},
@@ -553,7 +553,7 @@ var routeDescriptors = []RouteDescriptor{
 							referenceParameterDescriptor,
 							referenceParameterDescriptor,
 						},
 						},
 						Body: BodyDescriptor{
 						Body: BodyDescriptor{
-							ContentType: "application/json; charset=utf-8",
+							ContentType: "<media type of manifest>",
 							Format:      manifestBody,
 							Format:      manifestBody,
 						},
 						},
 						Successes: []ResponseDescriptor{
 						Successes: []ResponseDescriptor{

+ 216 - 70
vendor/src/github.com/docker/distribution/registry/client/auth/session.go

@@ -19,6 +19,8 @@ import (
 // basic auth due to lack of credentials.
 // basic auth due to lack of credentials.
 var ErrNoBasicAuthCredentials = errors.New("no basic auth credentials")
 var ErrNoBasicAuthCredentials = errors.New("no basic auth credentials")
 
 
+const defaultClientID = "registry-client"
+
 // AuthenticationHandler is an interface for authorizing a request from
 // AuthenticationHandler is an interface for authorizing a request from
 // params from a "WWW-Authenicate" header for a single scheme.
 // params from a "WWW-Authenicate" header for a single scheme.
 type AuthenticationHandler interface {
 type AuthenticationHandler interface {
@@ -36,6 +38,14 @@ type AuthenticationHandler interface {
 type CredentialStore interface {
 type CredentialStore interface {
 	// Basic returns basic auth for the given URL
 	// Basic returns basic auth for the given URL
 	Basic(*url.URL) (string, string)
 	Basic(*url.URL) (string, string)
+
+	// RefreshToken returns a refresh token for the
+	// given URL and service
+	RefreshToken(*url.URL, string) string
+
+	// SetRefreshToken sets the refresh token if none
+	// is provided for the given url and service
+	SetRefreshToken(realm *url.URL, service, token string)
 }
 }
 
 
 // NewAuthorizer creates an authorizer which can handle multiple authentication
 // NewAuthorizer creates an authorizer which can handle multiple authentication
@@ -105,27 +115,47 @@ type clock interface {
 type tokenHandler struct {
 type tokenHandler struct {
 	header    http.Header
 	header    http.Header
 	creds     CredentialStore
 	creds     CredentialStore
-	scope     tokenScope
 	transport http.RoundTripper
 	transport http.RoundTripper
 	clock     clock
 	clock     clock
 
 
+	offlineAccess bool
+	forceOAuth    bool
+	clientID      string
+	scopes        []Scope
+
 	tokenLock       sync.Mutex
 	tokenLock       sync.Mutex
 	tokenCache      string
 	tokenCache      string
 	tokenExpiration time.Time
 	tokenExpiration time.Time
+}
 
 
-	additionalScopes map[string]struct{}
+// Scope is a type which is serializable to a string
+// using the allow scope grammar.
+type Scope interface {
+	String() string
 }
 }
 
 
-// tokenScope represents the scope at which a token will be requested.
-// This represents a specific action on a registry resource.
-type tokenScope struct {
-	Resource string
-	Scope    string
-	Actions  []string
+// RepositoryScope represents a token scope for access
+// to a repository.
+type RepositoryScope struct {
+	Repository string
+	Actions    []string
 }
 }
 
 
-func (ts tokenScope) String() string {
-	return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ","))
+// String returns the string representation of the repository
+// using the scope grammar
+func (rs RepositoryScope) String() string {
+	return fmt.Sprintf("repository:%s:%s", rs.Repository, strings.Join(rs.Actions, ","))
+}
+
+// TokenHandlerOptions is used to configure a new token handler
+type TokenHandlerOptions struct {
+	Transport   http.RoundTripper
+	Credentials CredentialStore
+
+	OfflineAccess bool
+	ForceOAuth    bool
+	ClientID      string
+	Scopes        []Scope
 }
 }
 
 
 // An implementation of clock for providing real time data.
 // An implementation of clock for providing real time data.
@@ -137,22 +167,33 @@ func (realClock) Now() time.Time { return time.Now() }
 // NewTokenHandler creates a new AuthenicationHandler which supports
 // NewTokenHandler creates a new AuthenicationHandler which supports
 // fetching tokens from a remote token server.
 // fetching tokens from a remote token server.
 func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
 func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
-	return newTokenHandler(transport, creds, realClock{}, scope, actions...)
+	// Create options...
+	return NewTokenHandlerWithOptions(TokenHandlerOptions{
+		Transport:   transport,
+		Credentials: creds,
+		Scopes: []Scope{
+			RepositoryScope{
+				Repository: scope,
+				Actions:    actions,
+			},
+		},
+	})
 }
 }
 
 
-// newTokenHandler exposes the option to provide a clock to manipulate time in unit testing.
-func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock, scope string, actions ...string) AuthenticationHandler {
-	return &tokenHandler{
-		transport: transport,
-		creds:     creds,
-		clock:     c,
-		scope: tokenScope{
-			Resource: "repository",
-			Scope:    scope,
-			Actions:  actions,
-		},
-		additionalScopes: map[string]struct{}{},
+// NewTokenHandlerWithOptions creates a new token handler using the provided
+// options structure.
+func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler {
+	handler := &tokenHandler{
+		transport:     options.Transport,
+		creds:         options.Credentials,
+		offlineAccess: options.OfflineAccess,
+		forceOAuth:    options.ForceOAuth,
+		clientID:      options.ClientID,
+		scopes:        options.Scopes,
+		clock:         realClock{},
 	}
 	}
+
+	return handler
 }
 }
 
 
 func (th *tokenHandler) client() *http.Client {
 func (th *tokenHandler) client() *http.Client {
@@ -169,88 +210,162 @@ func (th *tokenHandler) Scheme() string {
 func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
 func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
 	var additionalScopes []string
 	var additionalScopes []string
 	if fromParam := req.URL.Query().Get("from"); fromParam != "" {
 	if fromParam := req.URL.Query().Get("from"); fromParam != "" {
-		additionalScopes = append(additionalScopes, tokenScope{
-			Resource: "repository",
-			Scope:    fromParam,
-			Actions:  []string{"pull"},
+		additionalScopes = append(additionalScopes, RepositoryScope{
+			Repository: fromParam,
+			Actions:    []string{"pull"},
 		}.String())
 		}.String())
 	}
 	}
-	if err := th.refreshToken(params, additionalScopes...); err != nil {
+
+	token, err := th.getToken(params, additionalScopes...)
+	if err != nil {
 		return err
 		return err
 	}
 	}
 
 
-	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.tokenCache))
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
 
 
 	return nil
 	return nil
 }
 }
 
 
-func (th *tokenHandler) refreshToken(params map[string]string, additionalScopes ...string) error {
+func (th *tokenHandler) getToken(params map[string]string, additionalScopes ...string) (string, error) {
 	th.tokenLock.Lock()
 	th.tokenLock.Lock()
 	defer th.tokenLock.Unlock()
 	defer th.tokenLock.Unlock()
+	scopes := make([]string, 0, len(th.scopes)+len(additionalScopes))
+	for _, scope := range th.scopes {
+		scopes = append(scopes, scope.String())
+	}
 	var addedScopes bool
 	var addedScopes bool
 	for _, scope := range additionalScopes {
 	for _, scope := range additionalScopes {
-		if _, ok := th.additionalScopes[scope]; !ok {
-			th.additionalScopes[scope] = struct{}{}
-			addedScopes = true
-		}
+		scopes = append(scopes, scope)
+		addedScopes = true
 	}
 	}
+
 	now := th.clock.Now()
 	now := th.clock.Now()
 	if now.After(th.tokenExpiration) || addedScopes {
 	if now.After(th.tokenExpiration) || addedScopes {
-		tr, err := th.fetchToken(params)
+		token, expiration, err := th.fetchToken(params, scopes)
 		if err != nil {
 		if err != nil {
-			return err
+			return "", err
+		}
+
+		// do not update cache for added scope tokens
+		if !addedScopes {
+			th.tokenCache = token
+			th.tokenExpiration = expiration
 		}
 		}
-		th.tokenCache = tr.Token
-		th.tokenExpiration = tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second)
+
+		return token, nil
 	}
 	}
 
 
-	return nil
+	return th.tokenCache, nil
 }
 }
 
 
-type tokenResponse struct {
-	Token       string    `json:"token"`
-	AccessToken string    `json:"access_token"`
-	ExpiresIn   int       `json:"expires_in"`
-	IssuedAt    time.Time `json:"issued_at"`
+type postTokenResponse struct {
+	AccessToken  string    `json:"access_token"`
+	RefreshToken string    `json:"refresh_token"`
+	ExpiresIn    int       `json:"expires_in"`
+	IssuedAt     time.Time `json:"issued_at"`
+	Scope        string    `json:"scope"`
 }
 }
 
 
-func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenResponse, err error) {
-	//log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username)
-	realm, ok := params["realm"]
-	if !ok {
-		return nil, errors.New("no realm specified for token auth challenge")
-	}
+func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) {
+	form := url.Values{}
+	form.Set("scope", strings.Join(scopes, " "))
+	form.Set("service", service)
 
 
-	// TODO(dmcgowan): Handle empty scheme
+	clientID := th.clientID
+	if clientID == "" {
+		// Use default client, this is a required field
+		clientID = defaultClientID
+	}
+	form.Set("client_id", clientID)
+
+	if refreshToken != "" {
+		form.Set("grant_type", "refresh_token")
+		form.Set("refresh_token", refreshToken)
+	} else if th.creds != nil {
+		form.Set("grant_type", "password")
+		username, password := th.creds.Basic(realm)
+		form.Set("username", username)
+		form.Set("password", password)
+
+		// attempt to get a refresh token
+		form.Set("access_type", "offline")
+	} else {
+		// refuse to do oauth without a grant type
+		return "", time.Time{}, fmt.Errorf("no supported grant type")
+	}
 
 
-	realmURL, err := url.Parse(realm)
+	resp, err := th.client().PostForm(realm.String(), form)
 	if err != nil {
 	if err != nil {
-		return nil, fmt.Errorf("invalid token auth challenge realm: %s", err)
+		return "", time.Time{}, err
+	}
+	defer resp.Body.Close()
+
+	if !client.SuccessStatus(resp.StatusCode) {
+		err := client.HandleErrorResponse(resp)
+		return "", time.Time{}, err
+	}
+
+	decoder := json.NewDecoder(resp.Body)
+
+	var tr postTokenResponse
+	if err = decoder.Decode(&tr); err != nil {
+		return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
+	}
+
+	if tr.RefreshToken != "" && tr.RefreshToken != refreshToken {
+		th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
+	}
+
+	if tr.ExpiresIn < minimumTokenLifetimeSeconds {
+		// The default/minimum lifetime.
+		tr.ExpiresIn = minimumTokenLifetimeSeconds
+		logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
 	}
 	}
 
 
-	req, err := http.NewRequest("GET", realmURL.String(), nil)
+	if tr.IssuedAt.IsZero() {
+		// issued_at is optional in the token response.
+		tr.IssuedAt = th.clock.Now().UTC()
+	}
+
+	return tr.AccessToken, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
+}
+
+type getTokenResponse struct {
+	Token        string    `json:"token"`
+	AccessToken  string    `json:"access_token"`
+	ExpiresIn    int       `json:"expires_in"`
+	IssuedAt     time.Time `json:"issued_at"`
+	RefreshToken string    `json:"refresh_token"`
+}
+
+func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string, scopes []string) (token string, expiration time.Time, err error) {
+
+	req, err := http.NewRequest("GET", realm.String(), nil)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return "", time.Time{}, err
 	}
 	}
 
 
 	reqParams := req.URL.Query()
 	reqParams := req.URL.Query()
-	service := params["service"]
-	scope := th.scope.String()
 
 
 	if service != "" {
 	if service != "" {
 		reqParams.Add("service", service)
 		reqParams.Add("service", service)
 	}
 	}
 
 
-	for _, scopeField := range strings.Fields(scope) {
-		reqParams.Add("scope", scopeField)
+	for _, scope := range scopes {
+		reqParams.Add("scope", scope)
 	}
 	}
 
 
-	for scope := range th.additionalScopes {
-		reqParams.Add("scope", scope)
+	if th.offlineAccess {
+		reqParams.Add("offline_token", "true")
+		clientID := th.clientID
+		if clientID == "" {
+			clientID = defaultClientID
+		}
+		reqParams.Add("client_id", clientID)
 	}
 	}
 
 
 	if th.creds != nil {
 	if th.creds != nil {
-		username, password := th.creds.Basic(realmURL)
+		username, password := th.creds.Basic(realm)
 		if username != "" && password != "" {
 		if username != "" && password != "" {
 			reqParams.Add("account", username)
 			reqParams.Add("account", username)
 			req.SetBasicAuth(username, password)
 			req.SetBasicAuth(username, password)
@@ -261,20 +376,24 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon
 
 
 	resp, err := th.client().Do(req)
 	resp, err := th.client().Do(req)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		return "", time.Time{}, err
 	}
 	}
 	defer resp.Body.Close()
 	defer resp.Body.Close()
 
 
 	if !client.SuccessStatus(resp.StatusCode) {
 	if !client.SuccessStatus(resp.StatusCode) {
 		err := client.HandleErrorResponse(resp)
 		err := client.HandleErrorResponse(resp)
-		return nil, err
+		return "", time.Time{}, err
 	}
 	}
 
 
 	decoder := json.NewDecoder(resp.Body)
 	decoder := json.NewDecoder(resp.Body)
 
 
-	tr := new(tokenResponse)
-	if err = decoder.Decode(tr); err != nil {
-		return nil, fmt.Errorf("unable to decode token response: %s", err)
+	var tr getTokenResponse
+	if err = decoder.Decode(&tr); err != nil {
+		return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
+	}
+
+	if tr.RefreshToken != "" && th.creds != nil {
+		th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
 	}
 	}
 
 
 	// `access_token` is equivalent to `token` and if both are specified
 	// `access_token` is equivalent to `token` and if both are specified
@@ -285,7 +404,7 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon
 	}
 	}
 
 
 	if tr.Token == "" {
 	if tr.Token == "" {
-		return nil, errors.New("authorization server did not include a token in the response")
+		return "", time.Time{}, errors.New("authorization server did not include a token in the response")
 	}
 	}
 
 
 	if tr.ExpiresIn < minimumTokenLifetimeSeconds {
 	if tr.ExpiresIn < minimumTokenLifetimeSeconds {
@@ -296,10 +415,37 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon
 
 
 	if tr.IssuedAt.IsZero() {
 	if tr.IssuedAt.IsZero() {
 		// issued_at is optional in the token response.
 		// issued_at is optional in the token response.
-		tr.IssuedAt = th.clock.Now()
+		tr.IssuedAt = th.clock.Now().UTC()
+	}
+
+	return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
+}
+
+func (th *tokenHandler) fetchToken(params map[string]string, scopes []string) (token string, expiration time.Time, err error) {
+	realm, ok := params["realm"]
+	if !ok {
+		return "", time.Time{}, errors.New("no realm specified for token auth challenge")
+	}
+
+	// TODO(dmcgowan): Handle empty scheme and relative realm
+	realmURL, err := url.Parse(realm)
+	if err != nil {
+		return "", time.Time{}, fmt.Errorf("invalid token auth challenge realm: %s", err)
+	}
+
+	service := params["service"]
+
+	var refreshToken string
+
+	if th.creds != nil {
+		refreshToken = th.creds.RefreshToken(realmURL, service)
+	}
+
+	if refreshToken != "" || th.forceOAuth {
+		return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes)
 	}
 	}
 
 
-	return tr, nil
+	return th.fetchTokenWithBasicAuth(realmURL, service, scopes)
 }
 }
 
 
 type basicHandler struct {
 type basicHandler struct {

+ 21 - 3
vendor/src/github.com/docker/distribution/registry/client/repository.go

@@ -292,9 +292,18 @@ func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, er
 	if err != nil {
 	if err != nil {
 		return distribution.Descriptor{}, err
 		return distribution.Descriptor{}, err
 	}
 	}
-	var attempts int
-	resp, err := t.client.Head(u)
 
 
+	req, err := http.NewRequest("HEAD", u, nil)
+	if err != nil {
+		return distribution.Descriptor{}, err
+	}
+
+	for _, t := range distribution.ManifestMediaTypes() {
+		req.Header.Add("Accept", t)
+	}
+
+	var attempts int
+	resp, err := t.client.Do(req)
 check:
 check:
 	if err != nil {
 	if err != nil {
 		return distribution.Descriptor{}, err
 		return distribution.Descriptor{}, err
@@ -304,7 +313,16 @@ check:
 	case resp.StatusCode >= 200 && resp.StatusCode < 400:
 	case resp.StatusCode >= 200 && resp.StatusCode < 400:
 		return descriptorFromResponse(resp)
 		return descriptorFromResponse(resp)
 	case resp.StatusCode == http.StatusMethodNotAllowed:
 	case resp.StatusCode == http.StatusMethodNotAllowed:
-		resp, err = t.client.Get(u)
+		req, err = http.NewRequest("GET", u, nil)
+		if err != nil {
+			return distribution.Descriptor{}, err
+		}
+
+		for _, t := range distribution.ManifestMediaTypes() {
+			req.Header.Add("Accept", t)
+		}
+
+		resp, err = t.client.Do(req)
 		attempts++
 		attempts++
 		if attempts > 1 {
 		if attempts > 1 {
 			return distribution.Descriptor{}, err
 			return distribution.Descriptor{}, err

+ 1 - 1
vendor/src/github.com/docker/distribution/registry/client/transport/http_reader.go

@@ -66,7 +66,7 @@ func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) {
 		return 0, hrs.err
 		return 0, hrs.err
 	}
 	}
 
 
-	// If we seeked to a different position, we need to reset the
+	// If we sought to a different position, we need to reset the
 	// connection. This logic is here instead of Seek so that if
 	// connection. This logic is here instead of Seek so that if
 	// a seek is undone before the next read, the connection doesn't
 	// a seek is undone before the next read, the connection doesn't
 	// need to be closed and reopened. A common example of this is
 	// need to be closed and reopened. A common example of this is