Skip to content

Commit b9361f0

Browse files
committed
Merge pull request moby#20970 from dmcgowan/login-oauth
OAuth support for registries
2 parents f480c69 + 76cd0f6 commit b9361f0

File tree

30 files changed

+470
-150
lines changed

30 files changed

+470
-150
lines changed

api/client/info.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ func (cli *DockerCli) CmdInfo(args ...string) error {
9393
u := cli.configFile.AuthConfigs[info.IndexServerAddress].Username
9494
if len(u) > 0 {
9595
fmt.Fprintf(cli.out, "Username: %v\n", u)
96-
fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress)
9796
}
97+
fmt.Fprintf(cli.out, "Registry: %v\n", info.IndexServerAddress)
9898
}
9999

100100
// Only output these warnings if the server does not support these features

api/client/login.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
5757
return err
5858
}
5959

60+
if response.IdentityToken != "" {
61+
authConfig.Password = ""
62+
authConfig.IdentityToken = response.IdentityToken
63+
}
6064
if err := storeCredentials(cli.configFile, authConfig); err != nil {
6165
return fmt.Errorf("Error saving credentials: %v", err)
6266
}
6367

6468
if response.Status != "" {
65-
fmt.Fprintf(cli.out, "%s\n", response.Status)
69+
fmt.Fprintln(cli.out, response.Status)
6670
}
6771
return nil
6872
}
@@ -120,6 +124,7 @@ func (cli *DockerCli) configureAuth(flUser, flPassword, serverAddress string, is
120124
authconfig.Username = flUser
121125
authconfig.Password = flPassword
122126
authconfig.ServerAddress = serverAddress
127+
authconfig.IdentityToken = ""
123128

124129
return authconfig, nil
125130
}

api/client/trust.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) {
107107
return scs.auth.Username, scs.auth.Password
108108
}
109109

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+
110117
// getNotaryRepository returns a NotaryRepository which stores all the
111118
// information needed to operate on a notary repository.
112119
// It creates a HTTP transport providing authentication support.

api/server/router/system/backend.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ type Backend interface {
1313
SystemVersion() types.Version
1414
SubscribeToEvents(since, sinceNano int64, ef filters.Args) ([]events.Message, chan interface{})
1515
UnsubscribeFromEvents(chan interface{})
16-
AuthenticateToRegistry(authConfig *types.AuthConfig) (string, error)
16+
AuthenticateToRegistry(authConfig *types.AuthConfig) (string, string, error)
1717
}

api/server/router/system/system_routes.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,12 @@ func (s *systemRouter) postAuth(ctx context.Context, w http.ResponseWriter, r *h
115115
if err != nil {
116116
return err
117117
}
118-
status, err := s.backend.AuthenticateToRegistry(config)
118+
status, token, err := s.backend.AuthenticateToRegistry(config)
119119
if err != nil {
120120
return err
121121
}
122122
return httputils.WriteJSON(w, http.StatusOK, &types.AuthResponse{
123-
Status: status,
123+
Status: status,
124+
IdentityToken: token,
124125
})
125126
}

cliconfig/credentials/native_store.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import (
1313
"github.com/docker/engine-api/types"
1414
)
1515

16-
const remoteCredentialsPrefix = "docker-credential-"
16+
const (
17+
remoteCredentialsPrefix = "docker-credential-"
18+
tokenUsername = "<token>"
19+
)
1720

1821
// Standarize the not found error, so every helper returns
1922
// the same message and docker can handle it properly.
@@ -29,14 +32,14 @@ type command interface {
2932
type credentialsRequest struct {
3033
ServerURL string
3134
Username string
32-
Password string
35+
Secret string
3336
}
3437

3538
// credentialsGetResponse is the information serialized from a remote store
3639
// when the plugin sends requests to get the user credentials.
3740
type credentialsGetResponse struct {
3841
Username string
39-
Password string
42+
Secret string
4043
}
4144

4245
// nativeStore implements a credentials store
@@ -76,6 +79,7 @@ func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) {
7679
return auth, err
7780
}
7881
auth.Username = creds.Username
82+
auth.IdentityToken = creds.IdentityToken
7983
auth.Password = creds.Password
8084

8185
return auth, nil
@@ -89,6 +93,7 @@ func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) {
8993
creds, _ := c.getCredentialsFromStore(s)
9094
ac.Username = creds.Username
9195
ac.Password = creds.Password
96+
ac.IdentityToken = creds.IdentityToken
9297
auths[s] = ac
9398
}
9499

@@ -102,6 +107,7 @@ func (c *nativeStore) Store(authConfig types.AuthConfig) error {
102107
}
103108
authConfig.Username = ""
104109
authConfig.Password = ""
110+
authConfig.IdentityToken = ""
105111

106112
// Fallback to old credential in plain text to save only the email
107113
return c.fileStore.Store(authConfig)
@@ -113,7 +119,12 @@ func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error {
113119
creds := &credentialsRequest{
114120
ServerURL: config.ServerAddress,
115121
Username: config.Username,
116-
Password: config.Password,
122+
Secret: config.Password,
123+
}
124+
125+
if config.IdentityToken != "" {
126+
creds.Username = tokenUsername
127+
creds.Secret = config.IdentityToken
117128
}
118129

119130
buffer := new(bytes.Buffer)
@@ -158,13 +169,18 @@ func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthC
158169
return ret, err
159170
}
160171

161-
ret.Username = resp.Username
162-
ret.Password = resp.Password
172+
if resp.Username == tokenUsername {
173+
ret.IdentityToken = resp.Secret
174+
} else {
175+
ret.Password = resp.Secret
176+
ret.Username = resp.Username
177+
}
178+
163179
ret.ServerAddress = serverAddress
164180
return ret, nil
165181
}
166182

167-
// eraseCredentialsFromStore executes the command to remove the server redentails from the native store.
183+
// eraseCredentialsFromStore executes the command to remove the server credentails from the native store.
168184
func (c *nativeStore) eraseCredentialsFromStore(serverURL string) error {
169185
cmd := c.commandFn("erase")
170186
cmd.Input(strings.NewReader(serverURL))

cliconfig/credentials/native_store_test.go

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ func (m *mockCommand) Output() ([]byte, error) {
4747
}
4848
case "get":
4949
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
5254
case missingCredsAddress:
5355
return []byte(errCredentialsNotFound.Error()), errCommandExited
5456
case invalidServerAddress:
@@ -118,6 +120,9 @@ func TestNativeStoreAddCredentials(t *testing.T) {
118120
if a.Password != "" {
119121
t.Fatalf("expected password to be empty, got %s", a.Password)
120122
}
123+
if a.IdentityToken != "" {
124+
t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
125+
}
121126
if a.Email != "[email protected]" {
122127
t.Fatalf("expected email `[email protected]`, got %s", a.Email)
123128
}
@@ -174,11 +179,45 @@ func TestNativeStoreGet(t *testing.T) {
174179
if a.Password != "bar" {
175180
t.Fatalf("expected password `bar`, got %s", a.Password)
176181
}
182+
if a.IdentityToken != "" {
183+
t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken)
184+
}
177185
if a.Email != "[email protected]" {
178186
t.Fatalf("expected email `[email protected]`, got %s", a.Email)
179187
}
180188
}
181189

190+
func TestNativeStoreGetIdentityToken(t *testing.T) {
191+
f := newConfigFile(map[string]types.AuthConfig{
192+
validServerAddress2: {
193+
194+
},
195+
})
196+
f.CredentialsStore = "mock"
197+
198+
s := &nativeStore{
199+
commandFn: mockCommandFn,
200+
fileStore: NewFileStore(f),
201+
}
202+
a, err := s.Get(validServerAddress2)
203+
if err != nil {
204+
t.Fatal(err)
205+
}
206+
207+
if a.Username != "" {
208+
t.Fatalf("expected username to be empty, got %s", a.Username)
209+
}
210+
if a.Password != "" {
211+
t.Fatalf("expected password to be empty, got %s", a.Password)
212+
}
213+
if a.IdentityToken != "abcd1234" {
214+
t.Fatalf("expected identity token `abcd1234`, got %s", a.IdentityToken)
215+
}
216+
if a.Email != "[email protected]" {
217+
t.Fatalf("expected email `[email protected]`, got %s", a.Email)
218+
}
219+
}
220+
182221
func TestNativeStoreGetAll(t *testing.T) {
183222
f := newConfigFile(map[string]types.AuthConfig{
184223
validServerAddress: {
@@ -209,14 +248,20 @@ func TestNativeStoreGetAll(t *testing.T) {
209248
if as[validServerAddress].Password != "bar" {
210249
t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password)
211250
}
251+
if as[validServerAddress].IdentityToken != "" {
252+
t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken)
253+
}
212254
if as[validServerAddress].Email != "[email protected]" {
213255
t.Fatalf("expected email `[email protected]` for %s, got %s", validServerAddress, as[validServerAddress].Email)
214256
}
215-
if as[validServerAddress2].Username != "foo" {
216-
t.Fatalf("expected username `foo` for %s, got %s", validServerAddress2, as[validServerAddress2].Username)
257+
if as[validServerAddress2].Username != "" {
258+
t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username)
259+
}
260+
if as[validServerAddress2].Password != "" {
261+
t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password)
217262
}
218-
if as[validServerAddress2].Password != "bar" {
219-
t.Fatalf("expected password `bar` for %s, got %s", validServerAddress2, as[validServerAddress2].Password)
263+
if as[validServerAddress2].IdentityToken != "abcd1234" {
264+
t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken)
220265
}
221266
if as[validServerAddress2].Email != "[email protected]" {
222267
t.Fatalf("expected email `[email protected]` for %s, got %s", validServerAddress2, as[validServerAddress2].Email)

daemon/daemon.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1519,7 +1519,7 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore,
15191519
}
15201520

15211521
// 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) {
15231523
return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent())
15241524
}
15251525

distribution/registry.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) {
2626
return dcs.auth.Username, dcs.auth.Password
2727
}
2828

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+
2936
// NewV2Repository returns a repository (v2 only). It creates a HTTP transport
3037
// providing timeout settings and authentication support, and also verifies the
3138
// remote API version.
@@ -72,7 +79,18 @@ func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, end
7279
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
7380
} else {
7481
creds := dumbCredentialStore{auth: authConfig}
75-
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...)
82+
tokenHandlerOptions := auth.TokenHandlerOptions{
83+
Transport: authTransport,
84+
Credentials: creds,
85+
Scopes: []auth.Scope{
86+
auth.RepositoryScope{
87+
Repository: repoName,
88+
Actions: actions,
89+
},
90+
},
91+
ClientID: registry.AuthClientID,
92+
}
93+
tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
7694
basicHandler := auth.NewBasicHandler(creds)
7795
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
7896
}

docs/reference/api/docker_remote_api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ This section lists each version from latest to oldest. Each listing includes a
125125
* `GET /info` now returns `KernelMemory` field, showing if "kernel memory limit" is supported.
126126
* `POST /containers/create` now takes `PidsLimit` field, if the kernel is >= 4.3 and the pids cgroup is supported.
127127
* `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.
128129

129130
### v1.22 API changes
130131

0 commit comments

Comments
 (0)