Browse code

Merge pull request #20970 from dmcgowan/login-oauth

OAuth support for registries

Vincent Demeester authored on 2016/03/14 23:49:44
Showing 30 changed files
... ...
@@ -93,8 +93,8 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
93 93
 		u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username
94 94
 		if len(u) > 0 {
95 95
 			fmt.Fprintf(cli.out, "Username: %v\n", u)
96
-			fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress)
97 96
 		}
97
+		fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress)
98 98
 	}
99 99
 
100 100
 	// Only output these warnings if the server does not support these features
... ...
@@ -57,12 +57,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
57 57
 		return err
58 58
 	}
59 59
 
60
+	if response.IdentityToken != "" {
61
+		authConfig.Password = ""
62
+		authConfig.IdentityToken = response.IdentityToken
63
+	}
60 64
 	if err := storeCredentials(cli.configFile, authConfig); err != nil {
61 65
 		return fmt.Errorf("Error saving credentials: %v", err)
62 66
 	}
63 67
 
64 68
 	if response.Status != "" {
65
-		fmt.Fprintf(cli.out, "%s\n", response.Status)
69
+		fmt.Fprintln(cli.out, response.Status)
66 70
 	}
67 71
 	return nil
68 72
 }
... ...
@@ -120,6 +124,7 @@ func (cli *DockerCli) configureAuth(flUser, flPassword, serverAddress string, is
120 120
 	authconfig.Username = flUser
121 121
 	authconfig.Password = flPassword
122 122
 	authconfig.ServerAddress = serverAddress
123
+	authconfig.IdentityToken = ""
123 124
 
124 125
 	return authconfig, nil
125 126
 }
... ...
@@ -107,6 +107,13 @@ func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) {
107 107
 	return scs.auth.Username, scs.auth.Password
108 108
 }
109 109
 
110
+func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string {
111
+	return scs.auth.IdentityToken
112
+}
113
+
114
+func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {
115
+}
116
+
110 117
 // getNotaryRepository returns a NotaryRepository which stores all the
111 118
 // information needed to operate on a notary repository.
112 119
 // It creates a HTTP transport providing authentication support.
... ...
@@ -13,5 +13,5 @@ type Backend interface {
13 13
 	SystemVersion() types.Version
14 14
 	SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{})
15 15
 	UnsubscribeFromEvents(chan interface{})
16
-	AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error)
16
+	AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error)
17 17
 }
... ...
@@ -115,11 +115,12 @@ func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *h
115 115
 	if err != nil {
116 116
 		return err
117 117
 	}
118
-	status, err := s.backend.AuthenticateToRegistry(config)
118
+	status, token, err := s.backend.AuthenticateToRegistry(config)
119 119
 	if err != nil {
120 120
 		return err
121 121
 	}
122 122
 	return httputils.WriteJSON(w, http.StatusOK, &types.AuthResponse{
123
-		Status: status,
123
+		Status:        status,
124
+		IdentityToken: token,
124 125
 	})
125 126
 }
... ...
@@ -13,7 +13,10 @@ import (
13 13
 	"github.com/docker/engine-api/types"
14 14
 )
15 15
 
16
-const remoteCredentialsPrefix = "docker-credential-"
16
+const (
17
+	remoteCredentialsPrefix = "docker-credential-"
18
+	tokenUsername           = "<token>"
19
+)
17 20
 
18 21
 // Standarize the not found error, so every helper returns
19 22
 // the same message and docker can handle it properly.
... ...
@@ -29,14 +32,14 @@ type command interface {
29 29
 type credentialsRequest struct {
30 30
 	ServerURL string
31 31
 	Username  string
32
-	Password  string
32
+	Secret    string
33 33
 }
34 34
 
35 35
 // credentialsGetResponse is the information serialized from a remote store
36 36
 // when the plugin sends requests to get the user credentials.
37 37
 type credentialsGetResponse struct {
38 38
 	Username string
39
-	Password string
39
+	Secret   string
40 40
 }
41 41
 
42 42
 // nativeStore implements a credentials store
... ...
@@ -76,6 +79,7 @@ func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) {
76 76
 		return auth, err
77 77
 	}
78 78
 	auth.Username = creds.Username
79
+	auth.IdentityToken = creds.IdentityToken
79 80
 	auth.Password = creds.Password
80 81
 
81 82
 	return auth, nil
... ...
@@ -89,6 +93,7 @@ func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) {
89 89
 		creds, _ := c.getCredentialsFromStore(s)
90 90
 		ac.Username = creds.Username
91 91
 		ac.Password = creds.Password
92
+		ac.IdentityToken = creds.IdentityToken
92 93
 		auths[s] = ac
93 94
 	}
94 95
 
... ...
@@ -102,6 +107,7 @@ func (c *nativeStore) Store(authConfig types.AuthConfig) error {
102 102
 	}
103 103
 	authConfig.Username = ""
104 104
 	authConfig.Password = ""
105
+	authConfig.IdentityToken = ""
105 106
 
106 107
 	// Fallback to old credential in plain text to save only the email
107 108
 	return c.fileStore.Store(authConfig)
... ...
@@ -113,7 +119,12 @@ func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
113 113
 	creds := &credentialsRequest{
114 114
 		ServerURL: config.ServerAddress,
115 115
 		Username:  config.Username,
116
-		Password:  config.Password,
116
+		Secret:    config.Password,
117
+	}
118
+
119
+	if config.IdentityToken != "" {
120
+		creds.Username = tokenUsername
121
+		creds.Secret = config.IdentityToken
117 122
 	}
118 123
 
119 124
 	buffer := new(bytes.Buffer)
... ...
@@ -158,13 +169,18 @@ func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthC
158 158
 		return ret, err
159 159
 	}
160 160
 
161
-	ret.Username = resp.Username
162
-	ret.Password = resp.Password
161
+	if resp.Username == tokenUsername {
162
+		ret.IdentityToken = resp.Secret
163
+	} else {
164
+		ret.Password = resp.Secret
165
+		ret.Username = resp.Username
166
+	}
167
+
163 168
 	ret.ServerAddress = serverAddress
164 169
 	return ret, nil
165 170
 }
166 171
 
167
-// eraseCredentialsFromStore executes the command to remove the server redentails from the native store.
172
+// eraseCredentialsFromStore executes the command to remove the server credentails from the native store.
168 173
 func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error {
169 174
 	cmd := c.commandFn("erase")
170 175
 	cmd.Input(strings.NewReader(serverURL))
... ...
@@ -47,8 +47,10 @@ func (m *mockCommand) Output() ([]byte, error) {
47 47
 		}
48 48
 	case "get":
49 49
 		switch inS {
50
-		case validServerAddress, validServerAddress2:
51
-			return []byte(`{"Username": "foo", "Password": "bar"}`), nil
50
+		case validServerAddress:
51
+			return []byte(`{"Username": "foo", "Secret": "bar"}`), nil
52
+		case validServerAddress2:
53
+			return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
52 54
 		case missingCredsAddress:
53 55
 			return []byte(errCredentialsNotFound.Error()), errCommandExited
54 56
 		case invalidServerAddress:
... ...
@@ -118,6 +120,9 @@ func TestNativeStoreAddCredentials(t *testing.T) {
118 118
 	if a.Password != "" {
119 119
 		t.Fatalf("expected password to be empty, got %s", a.Password)
120 120
 	}
121
+	if a.IdentityToken != "" {
122
+		t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
123
+	}
121 124
 	if a.Email != "foo@example.com" {
122 125
 		t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
123 126
 	}
... ...
@@ -174,11 +179,45 @@ func TestNativeStoreGet(t *testing.T) {
174 174
 	if a.Password != "bar" {
175 175
 		t.Fatalf("expected password `bar`, got %s", a.Password)
176 176
 	}
177
+	if a.IdentityToken != "" {
178
+		t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
179
+	}
177 180
 	if a.Email != "foo@example.com" {
178 181
 		t.Fatalf("expected email `foo@example.com`, got %s", a.Email)
179 182
 	}
180 183
 }
181 184
 
185
+func TestNativeStoreGetIdentityToken(t *testing.T) {
186
+	f := newConfigFile(map[string]types.AuthConfig{
187
+		validServerAddress2: {
188
+			Email: "foo@example2.com",
189
+		},
190
+	})
191
+	f.CredentialsStore = "mock"
192
+
193
+	s := &nativeStore{
194
+		commandFn: mockCommandFn,
195
+		fileStore: NewFileStore(f),
196
+	}
197
+	a, err := s.Get(validServerAddress2)
198
+	if err != nil {
199
+		t.Fatal(err)
200
+	}
201
+
202
+	if a.Username != "" {
203
+		t.Fatalf("expected username to be empty, got %s", a.Username)
204
+	}
205
+	if a.Password != "" {
206
+		t.Fatalf("expected password to be empty, got %s", a.Password)
207
+	}
208
+	if a.IdentityToken != "abcd1234" {
209
+		t.Fatalf("expected identity token `abcd1234`, got %s", a.IdentityToken)
210
+	}
211
+	if a.Email != "foo@example2.com" {
212
+		t.Fatalf("expected email `foo@example2.com`, got %s", a.Email)
213
+	}
214
+}
215
+
182 216
 func TestNativeStoreGetAll(t *testing.T) {
183 217
 	f := newConfigFile(map[string]types.AuthConfig{
184 218
 		validServerAddress: {
... ...
@@ -209,14 +248,20 @@ func TestNativeStoreGetAll(t *testing.T) {
209 209
 	if as[validServerAddress].Password != "bar" {
210 210
 		t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password)
211 211
 	}
212
+	if as[validServerAddress].IdentityToken != "" {
213
+		t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken)
214
+	}
212 215
 	if as[validServerAddress].Email != "foo@example.com" {
213 216
 		t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email)
214 217
 	}
215
-	if as[validServerAddress2].Username != "foo" {
216
-		t.Fatalf("expected username `foo` for %s, got %s", validServerAddress2, as[validServerAddress2].Username)
218
+	if as[validServerAddress2].Username != "" {
219
+		t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username)
220
+	}
221
+	if as[validServerAddress2].Password != "" {
222
+		t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password)
217 223
 	}
218
-	if as[validServerAddress2].Password != "bar" {
219
-		t.Fatalf("expected password `bar` for %s, got %s", validServerAddress2, as[validServerAddress2].Password)
224
+	if as[validServerAddress2].IdentityToken != "abcd1234" {
225
+		t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken)
220 226
 	}
221 227
 	if as[validServerAddress2].Email != "foo@example2.com" {
222 228
 		t.Fatalf("expected email `foo@example2.com` for %s, got %s", validServerAddress2, as[validServerAddress2].Email)
... ...
@@ -1519,7 +1519,7 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore,
1519 1519
 }
1520 1520
 
1521 1521
 // AuthenticateToRegistry checks the validity of credentials in authConfig
1522
-func (daemon *Daemon) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error) {
1522
+func (daemon *Daemon) AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error) {
1523 1523
 	return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent())
1524 1524
 }
1525 1525
 
... ...
@@ -26,6 +26,13 @@ func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) {
26 26
 	return dcs.auth.Username, dcs.auth.Password
27 27
 }
28 28
 
29
+func (dcs dumbCredentialStore) RefreshToken(*url.URL, string) string {
30
+	return dcs.auth.IdentityToken
31
+}
32
+
33
+func (dcs dumbCredentialStore) SetRefreshToken(*url.URL, string, string) {
34
+}
35
+
29 36
 // NewV2Repository returns a repository (v2 only). It creates a HTTP transport
30 37
 // providing timeout settings and authentication support, and also verifies the
31 38
 // remote API version.
... ...
@@ -72,7 +79,18 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
72 72
 		modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
73 73
 	} else {
74 74
 		creds := dumbCredentialStore{auth: authConfig}
75
-		tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...)
75
+		tokenHandlerOptions := auth.TokenHandlerOptions{
76
+			Transport:   authTransport,
77
+			Credentials: creds,
78
+			Scopes: []auth.Scope{
79
+				auth.RepositoryScope{
80
+					Repository: repoName,
81
+					Actions:    actions,
82
+				},
83
+			},
84
+			ClientID: registry.AuthClientID,
85
+		}
86
+		tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
76 87
 		basicHandler := auth.NewBasicHandler(creds)
77 88
 		modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
78 89
 	}
... ...
@@ -125,6 +125,7 @@ This section lists each version from latest to oldest.  Each listing includes a
125 125
 * `GET /info` now returns `KernelMemory` field, showing if "kernel memory limit" is supported.
126 126
 * `POST /containers/create` now takes `PidsLimit` field, if the kernel is >= 4.3 and the pids cgroup is supported.
127 127
 * `GET /containers/(id or name)/stats` now returns `pids_stats`, if the kernel is >= 4.3 and the pids cgroup is supported.
128
+* `POST /auth` now returns an `IdentityToken` when supported by a registry.
128 129
 
129 130
 ### v1.22 API changes
130 131
 
... ...
@@ -1985,11 +1985,11 @@ Request Headers:
1985 1985
     }
1986 1986
         ```
1987 1987
 
1988
-    - Token based login:
1988
+    - Identity token based login:
1989 1989
 
1990 1990
         ```
1991 1991
     {
1992
-            "registrytoken": "9cbaf023786cd7..."
1992
+            "identitytoken": "9cbaf023786cd7..."
1993 1993
     }
1994 1994
         ```
1995 1995
 
... ...
@@ -2119,7 +2119,8 @@ Status Codes:
2119 2119
 
2120 2120
 `POST /auth`
2121 2121
 
2122
-Get the default username and email
2122
+Validate credentials for a registry and get identity token,
2123
+if available, for accessing the registry without password.
2123 2124
 
2124 2125
 **Example request**:
2125 2126
 
... ...
@@ -2127,9 +2128,8 @@ Get the default username and email
2127 2127
     Content-Type: application/json
2128 2128
 
2129 2129
     {
2130
-         "username":" hannibal",
2131
-         "password: "xxxx",
2132
-         "email": "hannibal@a-team.com",
2130
+         "username": "hannibal",
2131
+         "password": "xxxx",
2133 2132
          "serveraddress": "https://index.docker.io/v1/"
2134 2133
     }
2135 2134
 
... ...
@@ -2137,6 +2137,11 @@ Get the default username and email
2137 2137
 
2138 2138
     HTTP/1.1 200 OK
2139 2139
 
2140
+    {
2141
+         "Status": "Login Succeeded",
2142
+         "IdentityToken": "9cbaf023786cd7..."
2143
+    }
2144
+
2140 2145
 Status Codes:
2141 2146
 
2142 2147
 -   **200** – no error
... ...
@@ -78,17 +78,20 @@ The helpers always use the first argument in the command to identify the action.
78 78
 There are only three possible values for that argument: `store`, `get`, and `erase`.
79 79
 
80 80
 The `store` command takes a JSON payload from the standard input. That payload carries
81
-the server address, to identify the credential, the user name and the password.
82
-This is an example of that payload:
81
+the server address, to identify the credential, the user name, and either a password
82
+or an identity token.
83 83
 
84 84
 ```json
85 85
 {
86 86
 	"ServerURL": "https://index.docker.io/v1",
87 87
 	"Username": "david",
88
-	"Password": "passw0rd1"
88
+	"Secret": "passw0rd1"
89 89
 }
90 90
 ```
91 91
 
92
+If the secret being stored is an identity token, the Username should be set to
93
+`<token>`.
94
+
92 95
 The `store` command can write error messages to `STDOUT` that the docker engine
93 96
 will show if there was an issue.
94 97
 
... ...
@@ -102,7 +105,7 @@ and password from this payload:
102 102
 ```json
103 103
 {
104 104
 	"Username": "david",
105
-	"Password": "passw0rd1"
105
+	"Secret": "passw0rd1"
106 106
 }
107 107
 ```
108 108
 
... ...
@@ -48,7 +48,7 @@ clone git github.com/boltdb/bolt v1.1.0
48 48
 clone git github.com/miekg/dns 75e6e86cc601825c5dbcd4e0c209eab180997cd7
49 49
 
50 50
 # get graph and distribution packages
51
-clone git github.com/docker/distribution 7b66c50bb7e0e4b3b83f8fd134a9f6ea4be08b57
51
+clone git github.com/docker/distribution db17a23b961978730892e12a0c6051d43a31aab3
52 52
 clone git github.com/vbatts/tar-split v0.9.11
53 53
 
54 54
 # get desired notary commit, might also need to be updated in Dockerfile
... ...
@@ -8,8 +8,8 @@ case $1 in
8 8
 		server=$(echo "$in" | jq --raw-output ".ServerURL" | sha1sum - | awk '{print $1}')
9 9
 
10 10
 		username=$(echo "$in" | jq --raw-output ".Username")
11
-		password=$(echo "$in" | jq --raw-output ".Password")
12
-		echo "{ \"Username\": \"${username}\", \"Password\": \"${password}\" }" > $TEMP/$server
11
+		password=$(echo "$in" | jq --raw-output ".Secret")
12
+		echo "{ \"Username\": \"${username}\", \"Secret\": \"${password}\" }" > $TEMP/$server
13 13
 		;;
14 14
 	"get")
15 15
 		in=$(</dev/stdin)
... ...
@@ -15,11 +15,16 @@ import (
15 15
 	registrytypes "github.com/docker/engine-api/types/registry"
16 16
 )
17 17
 
18
+const (
19
+	// AuthClientID is used the ClientID used for the token server
20
+	AuthClientID = "docker"
21
+)
22
+
18 23
 // loginV1 tries to register/login to the v1 registry server.
19
-func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, error) {
24
+func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, string, error) {
20 25
 	registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil)
21 26
 	if err != nil {
22
-		return "", err
27
+		return "", "", err
23 28
 	}
24 29
 
25 30
 	serverAddress := registryEndpoint.String()
... ...
@@ -27,48 +32,47 @@ func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent st
27 27
 	logrus.Debugf("attempting v1 login to registry endpoint %s", registryEndpoint)
28 28
 
29 29
 	if serverAddress == "" {
30
-		return "", fmt.Errorf("Server Error: Server Address not set.")
30
+		return "", "", fmt.Errorf("Server Error: Server Address not set.")
31 31
 	}
32 32
 
33 33
 	loginAgainstOfficialIndex := serverAddress == IndexServer
34 34
 
35 35
 	req, err := http.NewRequest("GET", serverAddress+"users/", nil)
36 36
 	if err != nil {
37
-		return "", err
37
+		return "", "", err
38 38
 	}
39 39
 	req.SetBasicAuth(authConfig.Username, authConfig.Password)
40 40
 	resp, err := registryEndpoint.client.Do(req)
41 41
 	if err != nil {
42 42
 		// fallback when request could not be completed
43
-		return "", fallbackError{
43
+		return "", "", fallbackError{
44 44
 			err: err,
45 45
 		}
46 46
 	}
47 47
 	defer resp.Body.Close()
48 48
 	body, err := ioutil.ReadAll(resp.Body)
49 49
 	if err != nil {
50
-		return "", err
50
+		return "", "", err
51 51
 	}
52 52
 	if resp.StatusCode == http.StatusOK {
53
-		return "Login Succeeded", nil
53
+		return "Login Succeeded", "", nil
54 54
 	} else if resp.StatusCode == http.StatusUnauthorized {
55 55
 		if loginAgainstOfficialIndex {
56
-			return "", fmt.Errorf("Wrong login/password, please try again. Haven't got a Docker ID? Create one at https://hub.docker.com")
56
+			return "", "", fmt.Errorf("Wrong login/password, please try again. Haven't got a Docker ID? Create one at https://hub.docker.com")
57 57
 		}
58
-		return "", fmt.Errorf("Wrong login/password, please try again")
58
+		return "", "", fmt.Errorf("Wrong login/password, please try again")
59 59
 	} else if resp.StatusCode == http.StatusForbidden {
60 60
 		if loginAgainstOfficialIndex {
61
-			return "", fmt.Errorf("Login: Account is not active. Please check your e-mail for a confirmation link.")
61
+			return "", "", fmt.Errorf("Login: Account is not active. Please check your e-mail for a confirmation link.")
62 62
 		}
63 63
 		// *TODO: Use registry configuration to determine what this says, if anything?
64
-		return "", fmt.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
64
+		return "", "", fmt.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
65 65
 	} else if resp.StatusCode == http.StatusInternalServerError { // Issue #14326
66 66
 		logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body)
67
-		return "", fmt.Errorf("Internal Server Error")
68
-	} else {
69
-		return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
70
-			resp.StatusCode, resp.Header)
67
+		return "", "", fmt.Errorf("Internal Server Error")
71 68
 	}
69
+	return "", "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
70
+		resp.StatusCode, resp.Header)
72 71
 }
73 72
 
74 73
 type loginCredentialStore struct {
... ...
@@ -79,6 +83,14 @@ func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
79 79
 	return lcs.authConfig.Username, lcs.authConfig.Password
80 80
 }
81 81
 
82
+func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string {
83
+	return lcs.authConfig.IdentityToken
84
+}
85
+
86
+func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) {
87
+	lcs.authConfig.IdentityToken = token
88
+}
89
+
82 90
 type fallbackError struct {
83 91
 	err error
84 92
 }
... ...
@@ -90,7 +102,7 @@ func (err fallbackError) Error() string {
90 90
 // loginV2 tries to login to the v2 registry server. The given registry
91 91
 // endpoint will be pinged to get authorization challenges. These challenges
92 92
 // will be used to authenticate against the registry to validate credentials.
93
-func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, error) {
93
+func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, string, error) {
94 94
 	logrus.Debugf("attempting v2 login to registry endpoint %s", endpoint)
95 95
 
96 96
 	modifiers := DockerHeaders(userAgent, nil)
... ...
@@ -101,14 +113,21 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
101 101
 		if !foundV2 {
102 102
 			err = fallbackError{err: err}
103 103
 		}
104
-		return "", err
104
+		return "", "", err
105 105
 	}
106 106
 
107
+	credentialAuthConfig := *authConfig
107 108
 	creds := loginCredentialStore{
108
-		authConfig: authConfig,
109
+		authConfig: &credentialAuthConfig,
109 110
 	}
110 111
 
111
-	tokenHandler := auth.NewTokenHandler(authTransport, creds, "")
112
+	tokenHandlerOptions := auth.TokenHandlerOptions{
113
+		Transport:     authTransport,
114
+		Credentials:   creds,
115
+		OfflineAccess: true,
116
+		ClientID:      AuthClientID,
117
+	}
118
+	tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
112 119
 	basicHandler := auth.NewBasicHandler(creds)
113 120
 	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
114 121
 	tr := transport.NewTransport(authTransport, modifiers...)
... ...
@@ -124,7 +143,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
124 124
 		if !foundV2 {
125 125
 			err = fallbackError{err: err}
126 126
 		}
127
-		return "", err
127
+		return "", "", err
128 128
 	}
129 129
 
130 130
 	resp, err := loginClient.Do(req)
... ...
@@ -132,7 +151,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
132 132
 		if !foundV2 {
133 133
 			err = fallbackError{err: err}
134 134
 		}
135
-		return "", err
135
+		return "", "", err
136 136
 	}
137 137
 	defer resp.Body.Close()
138 138
 
... ...
@@ -142,10 +161,10 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
142 142
 		if !foundV2 {
143 143
 			err = fallbackError{err: err}
144 144
 		}
145
-		return "", err
145
+		return "", "", err
146 146
 	}
147 147
 
148
-	return "Login Succeeded", nil
148
+	return "Login Succeeded", credentialAuthConfig.IdentityToken, nil
149 149
 
150 150
 }
151 151
 
... ...
@@ -2,6 +2,7 @@ package registry
2 2
 
3 3
 import (
4 4
 	"crypto/tls"
5
+	"fmt"
5 6
 	"net/http"
6 7
 	"net/url"
7 8
 	"strings"
... ...
@@ -34,10 +35,19 @@ func (s *Service) ServiceConfig() *registrytypes.ServiceConfig {
34 34
 // Auth contacts the public registry with the provided credentials,
35 35
 // and returns OK if authentication was successful.
36 36
 // It can be used to verify the validity of a client's credentials.
37
-func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status string, err error) {
38
-	endpoints, err := s.LookupPushEndpoints(authConfig.ServerAddress)
37
+func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status, token string, err error) {
38
+	serverAddress := authConfig.ServerAddress
39
+	if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
40
+		serverAddress = "https://" + serverAddress
41
+	}
42
+	u, err := url.Parse(serverAddress)
43
+	if err != nil {
44
+		return "", "", fmt.Errorf("unable to parse server address: %v", err)
45
+	}
46
+
47
+	endpoints, err := s.LookupPushEndpoints(u.Host)
39 48
 	if err != nil {
40
-		return "", err
49
+		return "", "", err
41 50
 	}
42 51
 
43 52
 	for _, endpoint := range endpoints {
... ...
@@ -46,7 +56,7 @@ func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status s
46 46
 			login = loginV1
47 47
 		}
48 48
 
49
-		status, err = login(authConfig, endpoint, userAgent)
49
+		status, token, err = login(authConfig, endpoint, userAgent)
50 50
 		if err == nil {
51 51
 			return
52 52
 		}
... ...
@@ -55,10 +65,10 @@ func (s *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status s
55 55
 			logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err)
56 56
 			continue
57 57
 		}
58
-		return "", err
58
+		return "", "", err
59 59
 	}
60 60
 
61
-	return "", err
61
+	return "", "", err
62 62
 }
63 63
 
64 64
 // splitReposSearchTerm breaks a search term into an index name and remote name
... ...
@@ -10,7 +10,7 @@ import (
10 10
 func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
11 11
 	var cfg = tlsconfig.ServerDefault
12 12
 	tlsConfig := &cfg
13
-	if hostname == DefaultNamespace {
13
+	if hostname == DefaultNamespace || hostname == DefaultV1Registry.Host {
14 14
 		// v2 mirrors
15 15
 		for _, mirror := range s.config.Mirrors {
16 16
 			if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
... ...
@@ -90,7 +90,7 @@ It's mandatory to:
90 90
 
91 91
 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.
92 92
 
93
-Have a look at a great, succesful contribution: the [Ceph driver PR](https://github.com/docker/distribution/pull/443)
93
+Have a look at a great, successful contribution: the [Ceph driver PR](https://github.com/docker/distribution/pull/443)
94 94
 
95 95
 ## Coding Style
96 96
 
... ...
@@ -16,4 +16,4 @@ RUN make PREFIX=/go clean binaries
16 16
 VOLUME ["/var/lib/registry"]
17 17
 EXPOSE 5000
18 18
 ENTRYPOINT ["registry"]
19
-CMD ["/etc/docker/registry/config.yml"]
19
+CMD ["serve", "/etc/docker/registry/config.yml"]
... ...
@@ -14,8 +14,8 @@ endif
14 14
 GO_LDFLAGS=-ldflags "-X `go list ./version`.Version=$(VERSION)"
15 15
 
16 16
 .PHONY: clean all fmt vet lint build test binaries
17
-.DEFAULT: default
18
-all: AUTHORS clean fmt vet fmt lint build test binaries
17
+.DEFAULT: all
18
+all: fmt vet fmt lint build test binaries
19 19
 
20 20
 AUTHORS: .mailmap .git/HEAD
21 21
 	 git log --format='%aN <%aE>' | sort -fu > $@
... ...
@@ -128,4 +128,4 @@ avenues are available for support:
128 128
 
129 129
 ## License
130 130
 
131
-This project is distributed under [Apache License, Version 2.0](LICENSE.md).
131
+This project is distributed under [Apache License, Version 2.0](LICENSE).
... ...
@@ -97,6 +97,11 @@ type BlobDeleter interface {
97 97
 	Delete(ctx context.Context, dgst digest.Digest) error
98 98
 }
99 99
 
100
+// BlobEnumerator enables iterating over blobs from storage
101
+type BlobEnumerator interface {
102
+	Enumerate(ctx context.Context, ingester func(dgst digest.Digest) error) error
103
+}
104
+
100 105
 // BlobDescriptorService manages metadata about a blob by digest. Most
101 106
 // implementations will not expose such an interface explicitly. Such mappings
102 107
 // should be maintained by interacting with the BlobIngester. Hence, this is
... ...
@@ -8,6 +8,10 @@ import (
8 8
 	"github.com/docker/distribution/digest"
9 9
 )
10 10
 
11
+// ErrAccessDenied is returned when an access to a requested resource is
12
+// denied.
13
+var ErrAccessDenied = errors.New("access denied")
14
+
11 15
 // ErrManifestNotModified is returned when a conditional manifest GetByTag
12 16
 // returns nil due to the client indicating it has the latest version
13 17
 var ErrManifestNotModified = errors.New("manifest not modified")
... ...
@@ -53,12 +53,18 @@ type ManifestService interface {
53 53
 	// Delete removes the manifest specified by the given digest. Deleting
54 54
 	// a manifest that doesn't exist will return ErrManifestNotFound
55 55
 	Delete(ctx context.Context, dgst digest.Digest) error
56
+}
57
+
58
+// ManifestEnumerator enables iterating over manifests
59
+type ManifestEnumerator interface {
60
+	// Enumerate calls ingester for each manifest.
61
+	Enumerate(ctx context.Context, ingester func(digest.Digest) error) error
62
+}
56 63
 
57
-	// Enumerate fills 'manifests' with the manifests in this service up
58
-	// to the size of 'manifests' and returns 'n' for the number of entries
59
-	// which were filled.  'last' contains an offset in the manifest set
60
-	// and can be used to resume iteration.
61
-	//Enumerate(ctx context.Context, manifests []Manifest, last Manifest) (n int, err error)
64
+// SignaturesGetter provides an interface for getting the signatures of a schema1 manifest. If the digest
65
+// referred to is not a schema1 manifest, an error should be returned.
66
+type SignaturesGetter interface {
67
+	GetSignatures(ctx context.Context, manifestDigest digest.Digest) ([]digest.Digest, error)
62 68
 }
63 69
 
64 70
 // Describable is an interface for descriptors
... ...
@@ -3,7 +3,7 @@
3 3
 //
4 4
 // Grammar
5 5
 //
6
-// 	reference                       := repository [ ":" tag ] [ "@" digest ]
6
+// 	reference                       := name [ ":" tag ] [ "@" digest ]
7 7
 //	name                            := [hostname '/'] component ['/' component]*
8 8
 //	hostname                        := hostcomponent ['.' hostcomponent]* [':' port-number]
9 9
 //	hostcomponent                   := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
... ...
@@ -40,6 +40,17 @@ type Namespace interface {
40 40
 	// which were filled.  'last' contains an offset in the catalog, and 'err' will be
41 41
 	// set to io.EOF if there are no more entries to obtain.
42 42
 	Repositories(ctx context.Context, repos []string, last string) (n int, err error)
43
+
44
+	// Blobs returns a blob enumerator to access all blobs
45
+	Blobs() BlobEnumerator
46
+
47
+	// BlobStatter returns a BlobStatter to control
48
+	BlobStatter() BlobStatter
49
+}
50
+
51
+// RepositoryEnumerator describes an operation to enumerate repositories
52
+type RepositoryEnumerator interface {
53
+	Enumerate(ctx context.Context, ingester func(string) error) error
43 54
 }
44 55
 
45 56
 // ManifestServiceOption is a function argument for Manifest Service methods
... ...
@@ -514,7 +514,7 @@ var routeDescriptors = []RouteDescriptor{
514 514
 									digestHeader,
515 515
 								},
516 516
 								Body: BodyDescriptor{
517
-									ContentType: "application/json; charset=utf-8",
517
+									ContentType: "<media type of manifest>",
518 518
 									Format:      manifestBody,
519 519
 								},
520 520
 							},
... ...
@@ -553,7 +553,7 @@ var routeDescriptors = []RouteDescriptor{
553 553
 							referenceParameterDescriptor,
554 554
 						},
555 555
 						Body: BodyDescriptor{
556
-							ContentType: "application/json; charset=utf-8",
556
+							ContentType: "<media type of manifest>",
557 557
 							Format:      manifestBody,
558 558
 						},
559 559
 						Successes: []ResponseDescriptor{
... ...
@@ -19,6 +19,8 @@ import (
19 19
 // basic auth due to lack of credentials.
20 20
 var ErrNoBasicAuthCredentials = errors.New("no basic auth credentials")
21 21
 
22
+const defaultClientID = "registry-client"
23
+
22 24
 // AuthenticationHandler is an interface for authorizing a request from
23 25
 // params from a "WWW-Authenicate" header for a single scheme.
24 26
 type AuthenticationHandler interface {
... ...
@@ -36,6 +38,14 @@ type AuthenticationHandler interface {
36 36
 type CredentialStore interface {
37 37
 	// Basic returns basic auth for the given URL
38 38
 	Basic(*url.URL) (string, string)
39
+
40
+	// RefreshToken returns a refresh token for the
41
+	// given URL and service
42
+	RefreshToken(*url.URL, string) string
43
+
44
+	// SetRefreshToken sets the refresh token if none
45
+	// is provided for the given url and service
46
+	SetRefreshToken(realm *url.URL, service, token string)
39 47
 }
40 48
 
41 49
 // NewAuthorizer creates an authorizer which can handle multiple authentication
... ...
@@ -105,27 +115,47 @@ type clock interface {
105 105
 type tokenHandler struct {
106 106
 	header    http.Header
107 107
 	creds     CredentialStore
108
-	scope     tokenScope
109 108
 	transport http.RoundTripper
110 109
 	clock     clock
111 110
 
111
+	offlineAccess bool
112
+	forceOAuth    bool
113
+	clientID      string
114
+	scopes        []Scope
115
+
112 116
 	tokenLock       sync.Mutex
113 117
 	tokenCache      string
114 118
 	tokenExpiration time.Time
119
+}
115 120
 
116
-	additionalScopes map[string]struct{}
121
+// Scope is a type which is serializable to a string
122
+// using the allow scope grammar.
123
+type Scope interface {
124
+	String() string
117 125
 }
118 126
 
119
-// tokenScope represents the scope at which a token will be requested.
120
-// This represents a specific action on a registry resource.
121
-type tokenScope struct {
122
-	Resource string
123
-	Scope    string
124
-	Actions  []string
127
+// RepositoryScope represents a token scope for access
128
+// to a repository.
129
+type RepositoryScope struct {
130
+	Repository string
131
+	Actions    []string
125 132
 }
126 133
 
127
-func (ts tokenScope) String() string {
128
-	return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ","))
134
+// String returns the string representation of the repository
135
+// using the scope grammar
136
+func (rs RepositoryScope) String() string {
137
+	return fmt.Sprintf("repository:%s:%s", rs.Repository, strings.Join(rs.Actions, ","))
138
+}
139
+
140
+// TokenHandlerOptions is used to configure a new token handler
141
+type TokenHandlerOptions struct {
142
+	Transport   http.RoundTripper
143
+	Credentials CredentialStore
144
+
145
+	OfflineAccess bool
146
+	ForceOAuth    bool
147
+	ClientID      string
148
+	Scopes        []Scope
129 149
 }
130 150
 
131 151
 // An implementation of clock for providing real time data.
... ...
@@ -137,22 +167,33 @@ func (realClock) Now() time.Time { return time.Now() }
137 137
 // NewTokenHandler creates a new AuthenicationHandler which supports
138 138
 // fetching tokens from a remote token server.
139 139
 func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
140
-	return newTokenHandler(transport, creds, realClock{}, scope, actions...)
140
+	// Create options...
141
+	return NewTokenHandlerWithOptions(TokenHandlerOptions{
142
+		Transport:   transport,
143
+		Credentials: creds,
144
+		Scopes: []Scope{
145
+			RepositoryScope{
146
+				Repository: scope,
147
+				Actions:    actions,
148
+			},
149
+		},
150
+	})
141 151
 }
142 152
 
143
-// newTokenHandler exposes the option to provide a clock to manipulate time in unit testing.
144
-func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock, scope string, actions ...string) AuthenticationHandler {
145
-	return &tokenHandler{
146
-		transport: transport,
147
-		creds:     creds,
148
-		clock:     c,
149
-		scope: tokenScope{
150
-			Resource: "repository",
151
-			Scope:    scope,
152
-			Actions:  actions,
153
-		},
154
-		additionalScopes: map[string]struct{}{},
153
+// NewTokenHandlerWithOptions creates a new token handler using the provided
154
+// options structure.
155
+func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler {
156
+	handler := &tokenHandler{
157
+		transport:     options.Transport,
158
+		creds:         options.Credentials,
159
+		offlineAccess: options.OfflineAccess,
160
+		forceOAuth:    options.ForceOAuth,
161
+		clientID:      options.ClientID,
162
+		scopes:        options.Scopes,
163
+		clock:         realClock{},
155 164
 	}
165
+
166
+	return handler
156 167
 }
157 168
 
158 169
 func (th *tokenHandler) client() *http.Client {
... ...
@@ -169,88 +210,162 @@ func (th *tokenHandler) Scheme() string {
169 169
 func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
170 170
 	var additionalScopes []string
171 171
 	if fromParam := req.URL.Query().Get("from"); fromParam != "" {
172
-		additionalScopes = append(additionalScopes, tokenScope{
173
-			Resource: "repository",
174
-			Scope:    fromParam,
175
-			Actions:  []string{"pull"},
172
+		additionalScopes = append(additionalScopes, RepositoryScope{
173
+			Repository: fromParam,
174
+			Actions:    []string{"pull"},
176 175
 		}.String())
177 176
 	}
178
-	if err := th.refreshToken(params, additionalScopes...); err != nil {
177
+
178
+	token, err := th.getToken(params, additionalScopes...)
179
+	if err != nil {
179 180
 		return err
180 181
 	}
181 182
 
182
-	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.tokenCache))
183
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
183 184
 
184 185
 	return nil
185 186
 }
186 187
 
187
-func (th *tokenHandler) refreshToken(params map[string]string, additionalScopes ...string) error {
188
+func (th *tokenHandler) getToken(params map[string]string, additionalScopes ...string) (string, error) {
188 189
 	th.tokenLock.Lock()
189 190
 	defer th.tokenLock.Unlock()
191
+	scopes := make([]string, 0, len(th.scopes)+len(additionalScopes))
192
+	for _, scope := range th.scopes {
193
+		scopes = append(scopes, scope.String())
194
+	}
190 195
 	var addedScopes bool
191 196
 	for _, scope := range additionalScopes {
192
-		if _, ok := th.additionalScopes[scope]; !ok {
193
-			th.additionalScopes[scope] = struct{}{}
194
-			addedScopes = true
195
-		}
197
+		scopes = append(scopes, scope)
198
+		addedScopes = true
196 199
 	}
200
+
197 201
 	now := th.clock.Now()
198 202
 	if now.After(th.tokenExpiration) || addedScopes {
199
-		tr, err := th.fetchToken(params)
203
+		token, expiration, err := th.fetchToken(params, scopes)
200 204
 		if err != nil {
201
-			return err
205
+			return "", err
206
+		}
207
+
208
+		// do not update cache for added scope tokens
209
+		if !addedScopes {
210
+			th.tokenCache = token
211
+			th.tokenExpiration = expiration
202 212
 		}
203
-		th.tokenCache = tr.Token
204
-		th.tokenExpiration = tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second)
213
+
214
+		return token, nil
205 215
 	}
206 216
 
207
-	return nil
217
+	return th.tokenCache, nil
208 218
 }
209 219
 
210
-type tokenResponse struct {
211
-	Token       string    `json:"token"`
212
-	AccessToken string    `json:"access_token"`
213
-	ExpiresIn   int       `json:"expires_in"`
214
-	IssuedAt    time.Time `json:"issued_at"`
220
+type postTokenResponse struct {
221
+	AccessToken  string    `json:"access_token"`
222
+	RefreshToken string    `json:"refresh_token"`
223
+	ExpiresIn    int       `json:"expires_in"`
224
+	IssuedAt     time.Time `json:"issued_at"`
225
+	Scope        string    `json:"scope"`
215 226
 }
216 227
 
217
-func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenResponse, err error) {
218
-	//log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username)
219
-	realm, ok := params["realm"]
220
-	if !ok {
221
-		return nil, errors.New("no realm specified for token auth challenge")
222
-	}
228
+func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) {
229
+	form := url.Values{}
230
+	form.Set("scope", strings.Join(scopes, " "))
231
+	form.Set("service", service)
223 232
 
224
-	// TODO(dmcgowan): Handle empty scheme
233
+	clientID := th.clientID
234
+	if clientID == "" {
235
+		// Use default client, this is a required field
236
+		clientID = defaultClientID
237
+	}
238
+	form.Set("client_id", clientID)
239
+
240
+	if refreshToken != "" {
241
+		form.Set("grant_type", "refresh_token")
242
+		form.Set("refresh_token", refreshToken)
243
+	} else if th.creds != nil {
244
+		form.Set("grant_type", "password")
245
+		username, password := th.creds.Basic(realm)
246
+		form.Set("username", username)
247
+		form.Set("password", password)
248
+
249
+		// attempt to get a refresh token
250
+		form.Set("access_type", "offline")
251
+	} else {
252
+		// refuse to do oauth without a grant type
253
+		return "", time.Time{}, fmt.Errorf("no supported grant type")
254
+	}
225 255
 
226
-	realmURL, err := url.Parse(realm)
256
+	resp, err := th.client().PostForm(realm.String(), form)
227 257
 	if err != nil {
228
-		return nil, fmt.Errorf("invalid token auth challenge realm: %s", err)
258
+		return "", time.Time{}, err
259
+	}
260
+	defer resp.Body.Close()
261
+
262
+	if !client.SuccessStatus(resp.StatusCode) {
263
+		err := client.HandleErrorResponse(resp)
264
+		return "", time.Time{}, err
265
+	}
266
+
267
+	decoder := json.NewDecoder(resp.Body)
268
+
269
+	var tr postTokenResponse
270
+	if err = decoder.Decode(&tr); err != nil {
271
+		return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
272
+	}
273
+
274
+	if tr.RefreshToken != "" && tr.RefreshToken != refreshToken {
275
+		th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
276
+	}
277
+
278
+	if tr.ExpiresIn < minimumTokenLifetimeSeconds {
279
+		// The default/minimum lifetime.
280
+		tr.ExpiresIn = minimumTokenLifetimeSeconds
281
+		logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
229 282
 	}
230 283
 
231
-	req, err := http.NewRequest("GET", realmURL.String(), nil)
284
+	if tr.IssuedAt.IsZero() {
285
+		// issued_at is optional in the token response.
286
+		tr.IssuedAt = th.clock.Now().UTC()
287
+	}
288
+
289
+	return tr.AccessToken, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
290
+}
291
+
292
+type getTokenResponse struct {
293
+	Token        string    `json:"token"`
294
+	AccessToken  string    `json:"access_token"`
295
+	ExpiresIn    int       `json:"expires_in"`
296
+	IssuedAt     time.Time `json:"issued_at"`
297
+	RefreshToken string    `json:"refresh_token"`
298
+}
299
+
300
+func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string, scopes []string) (token string, expiration time.Time, err error) {
301
+
302
+	req, err := http.NewRequest("GET", realm.String(), nil)
232 303
 	if err != nil {
233
-		return nil, err
304
+		return "", time.Time{}, err
234 305
 	}
235 306
 
236 307
 	reqParams := req.URL.Query()
237
-	service := params["service"]
238
-	scope := th.scope.String()
239 308
 
240 309
 	if service != "" {
241 310
 		reqParams.Add("service", service)
242 311
 	}
243 312
 
244
-	for _, scopeField := range strings.Fields(scope) {
245
-		reqParams.Add("scope", scopeField)
313
+	for _, scope := range scopes {
314
+		reqParams.Add("scope", scope)
246 315
 	}
247 316
 
248
-	for scope := range th.additionalScopes {
249
-		reqParams.Add("scope", scope)
317
+	if th.offlineAccess {
318
+		reqParams.Add("offline_token", "true")
319
+		clientID := th.clientID
320
+		if clientID == "" {
321
+			clientID = defaultClientID
322
+		}
323
+		reqParams.Add("client_id", clientID)
250 324
 	}
251 325
 
252 326
 	if th.creds != nil {
253
-		username, password := th.creds.Basic(realmURL)
327
+		username, password := th.creds.Basic(realm)
254 328
 		if username != "" && password != "" {
255 329
 			reqParams.Add("account", username)
256 330
 			req.SetBasicAuth(username, password)
... ...
@@ -261,20 +376,24 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon
261 261
 
262 262
 	resp, err := th.client().Do(req)
263 263
 	if err != nil {
264
-		return nil, err
264
+		return "", time.Time{}, err
265 265
 	}
266 266
 	defer resp.Body.Close()
267 267
 
268 268
 	if !client.SuccessStatus(resp.StatusCode) {
269 269
 		err := client.HandleErrorResponse(resp)
270
-		return nil, err
270
+		return "", time.Time{}, err
271 271
 	}
272 272
 
273 273
 	decoder := json.NewDecoder(resp.Body)
274 274
 
275
-	tr := new(tokenResponse)
276
-	if err = decoder.Decode(tr); err != nil {
277
-		return nil, fmt.Errorf("unable to decode token response: %s", err)
275
+	var tr getTokenResponse
276
+	if err = decoder.Decode(&tr); err != nil {
277
+		return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
278
+	}
279
+
280
+	if tr.RefreshToken != "" && th.creds != nil {
281
+		th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
278 282
 	}
279 283
 
280 284
 	// `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
285 285
 	}
286 286
 
287 287
 	if tr.Token == "" {
288
-		return nil, errors.New("authorization server did not include a token in the response")
288
+		return "", time.Time{}, errors.New("authorization server did not include a token in the response")
289 289
 	}
290 290
 
291 291
 	if tr.ExpiresIn < minimumTokenLifetimeSeconds {
... ...
@@ -296,10 +415,37 @@ func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenRespon
296 296
 
297 297
 	if tr.IssuedAt.IsZero() {
298 298
 		// issued_at is optional in the token response.
299
-		tr.IssuedAt = th.clock.Now()
299
+		tr.IssuedAt = th.clock.Now().UTC()
300
+	}
301
+
302
+	return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
303
+}
304
+
305
+func (th *tokenHandler) fetchToken(params map[string]string, scopes []string) (token string, expiration time.Time, err error) {
306
+	realm, ok := params["realm"]
307
+	if !ok {
308
+		return "", time.Time{}, errors.New("no realm specified for token auth challenge")
309
+	}
310
+
311
+	// TODO(dmcgowan): Handle empty scheme and relative realm
312
+	realmURL, err := url.Parse(realm)
313
+	if err != nil {
314
+		return "", time.Time{}, fmt.Errorf("invalid token auth challenge realm: %s", err)
315
+	}
316
+
317
+	service := params["service"]
318
+
319
+	var refreshToken string
320
+
321
+	if th.creds != nil {
322
+		refreshToken = th.creds.RefreshToken(realmURL, service)
323
+	}
324
+
325
+	if refreshToken != "" || th.forceOAuth {
326
+		return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes)
300 327
 	}
301 328
 
302
-	return tr, nil
329
+	return th.fetchTokenWithBasicAuth(realmURL, service, scopes)
303 330
 }
304 331
 
305 332
 type basicHandler struct {
... ...
@@ -292,9 +292,18 @@ func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, er
292 292
 	if err != nil {
293 293
 		return distribution.Descriptor{}, err
294 294
 	}
295
-	var attempts int
296
-	resp, err := t.client.Head(u)
297 295
 
296
+	req, err := http.NewRequest("HEAD", u, nil)
297
+	if err != nil {
298
+		return distribution.Descriptor{}, err
299
+	}
300
+
301
+	for _, t := range distribution.ManifestMediaTypes() {
302
+		req.Header.Add("Accept", t)
303
+	}
304
+
305
+	var attempts int
306
+	resp, err := t.client.Do(req)
298 307
 check:
299 308
 	if err != nil {
300 309
 		return distribution.Descriptor{}, err
... ...
@@ -304,7 +313,16 @@ check:
304 304
 	case resp.StatusCode >= 200 && resp.StatusCode < 400:
305 305
 		return descriptorFromResponse(resp)
306 306
 	case resp.StatusCode == http.StatusMethodNotAllowed:
307
-		resp, err = t.client.Get(u)
307
+		req, err = http.NewRequest("GET", u, nil)
308
+		if err != nil {
309
+			return distribution.Descriptor{}, err
310
+		}
311
+
312
+		for _, t := range distribution.ManifestMediaTypes() {
313
+			req.Header.Add("Accept", t)
314
+		}
315
+
316
+		resp, err = t.client.Do(req)
308 317
 		attempts++
309 318
 		if attempts > 1 {
310 319
 			return distribution.Descriptor{}, err
... ...
@@ -66,7 +66,7 @@ func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) {
66 66
 		return 0, hrs.err
67 67
 	}
68 68
 
69
-	// If we seeked to a different position, we need to reset the
69
+	// If we sought to a different position, we need to reset the
70 70
 	// connection. This logic is here instead of Seek so that if
71 71
 	// a seek is undone before the next read, the connection doesn't
72 72
 	// need to be closed and reopened. A common example of this is