Skip to content

Commit bf94267

Browse files
authored
Merge pull request #829 from yihuaf/dev/yihuaf/authfile
Support IdentityToken in registry authn
2 parents d1d30d0 + 31d443d commit bf94267

File tree

5 files changed

+293
-138
lines changed

5 files changed

+293
-138
lines changed

docker/docker_client.go

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package docker
22

33
import (
4+
"bytes"
45
"context"
56
"crypto/tls"
67
"encoding/json"
78
"fmt"
89
"io"
10+
"io/ioutil"
911
"net/http"
1012
"net/url"
1113
"os"
@@ -97,8 +99,7 @@ type dockerClient struct {
9799
// by detectProperties(). Callers can edit tlsClientConfig.InsecureSkipVerify in the meantime.
98100
tlsClientConfig *tls.Config
99101
// The following members are not set by newDockerClient and must be set by callers if needed.
100-
username string
101-
password string
102+
auth types.DockerAuthConfig
102103
registryToken string
103104
signatureBase signatureStorageBase
104105
scope authScope
@@ -210,10 +211,11 @@ func dockerCertDir(sys *types.SystemContext, hostPort string) (string, error) {
210211
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
211212
func newDockerClientFromRef(sys *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) {
212213
registry := reference.Domain(ref.ref)
213-
username, password, err := config.GetAuthentication(sys, registry)
214+
auth, err := config.GetCredentials(sys, registry)
214215
if err != nil {
215216
return nil, errors.Wrapf(err, "error getting username and password")
216217
}
218+
217219
sigBase, err := configuredSignatureStorageBase(sys, ref, write)
218220
if err != nil {
219221
return nil, err
@@ -223,8 +225,7 @@ func newDockerClientFromRef(sys *types.SystemContext, ref dockerReference, write
223225
if err != nil {
224226
return nil, err
225227
}
226-
client.username = username
227-
client.password = password
228+
client.auth = auth
228229
if sys != nil {
229230
client.registryToken = sys.DockerBearerRegistryToken
230231
}
@@ -289,8 +290,10 @@ func CheckAuth(ctx context.Context, sys *types.SystemContext, username, password
289290
if err != nil {
290291
return errors.Wrapf(err, "error creating new docker client")
291292
}
292-
client.username = username
293-
client.password = password
293+
client.auth = types.DockerAuthConfig{
294+
Username: username,
295+
Password: password,
296+
}
294297

295298
resp, err := client.makeRequest(ctx, "GET", "/v2/", nil, nil, v2Auth, nil)
296299
if err != nil {
@@ -332,7 +335,7 @@ func SearchRegistry(ctx context.Context, sys *types.SystemContext, registry, ima
332335
v1Res := &V1Results{}
333336

334337
// Get credentials from authfile for the underlying hostname
335-
username, password, err := config.GetAuthentication(sys, registry)
338+
auth, err := config.GetCredentials(sys, registry)
336339
if err != nil {
337340
return nil, errors.Wrapf(err, "error getting username and password")
338341
}
@@ -350,8 +353,7 @@ func SearchRegistry(ctx context.Context, sys *types.SystemContext, registry, ima
350353
if err != nil {
351354
return nil, errors.Wrapf(err, "error creating new docker client")
352355
}
353-
client.username = username
354-
client.password = password
356+
client.auth = auth
355357
if sys != nil {
356358
client.registryToken = sys.DockerBearerRegistryToken
357359
}
@@ -535,7 +537,7 @@ func (c *dockerClient) setupRequestAuth(req *http.Request, extraScope *authScope
535537
schemeNames = append(schemeNames, challenge.Scheme)
536538
switch challenge.Scheme {
537539
case "basic":
538-
req.SetBasicAuth(c.username, c.password)
540+
req.SetBasicAuth(c.auth.Username, c.auth.Password)
539541
return nil
540542
case "bearer":
541543
registryToken := c.registryToken
@@ -553,10 +555,19 @@ func (c *dockerClient) setupRequestAuth(req *http.Request, extraScope *authScope
553555
token = t.(bearerToken)
554556
}
555557
if !inCache || time.Now().After(token.expirationTime) {
556-
t, err := c.getBearerToken(req.Context(), challenge, scopes)
558+
var (
559+
t *bearerToken
560+
err error
561+
)
562+
if c.auth.IdentityToken != "" {
563+
t, err = c.getBearerTokenOAuth2(req.Context(), challenge, scopes)
564+
} else {
565+
t, err = c.getBearerToken(req.Context(), challenge, scopes)
566+
}
557567
if err != nil {
558568
return err
559569
}
570+
560571
token = *t
561572
c.tokenCache.Store(cacheKey, token)
562573
}
@@ -572,48 +583,96 @@ func (c *dockerClient) setupRequestAuth(req *http.Request, extraScope *authScope
572583
return nil
573584
}
574585

575-
func (c *dockerClient) getBearerToken(ctx context.Context, challenge challenge, scopes []authScope) (*bearerToken, error) {
586+
func (c *dockerClient) getBearerTokenOAuth2(ctx context.Context, challenge challenge,
587+
scopes []authScope) (*bearerToken, error) {
588+
realm, ok := challenge.Parameters["realm"]
589+
if !ok {
590+
return nil, errors.Errorf("missing realm in bearer auth challenge")
591+
}
592+
593+
authReq, err := http.NewRequest(http.MethodPost, realm, nil)
594+
if err != nil {
595+
return nil, err
596+
}
597+
598+
authReq = authReq.WithContext(ctx)
599+
600+
// Make the form data required against the oauth2 authentication
601+
// More details here: https://docs.docker.com/registry/spec/auth/oauth/
602+
params := authReq.URL.Query()
603+
if service, ok := challenge.Parameters["service"]; ok && service != "" {
604+
params.Add("service", service)
605+
}
606+
for _, scope := range scopes {
607+
if scope.remoteName != "" && scope.actions != "" {
608+
params.Add("scope", fmt.Sprintf("repository:%s:%s", scope.remoteName, scope.actions))
609+
}
610+
}
611+
params.Add("grant_type", "refresh_token")
612+
params.Add("refresh_token", c.auth.IdentityToken)
613+
614+
authReq.Body = ioutil.NopCloser(bytes.NewBufferString(params.Encode()))
615+
authReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
616+
logrus.Debugf("%s %s", authReq.Method, authReq.URL.String())
617+
res, err := c.client.Do(authReq)
618+
if err != nil {
619+
return nil, err
620+
}
621+
defer res.Body.Close()
622+
if err := httpResponseToError(res, "Trying to obtain access token"); err != nil {
623+
return nil, err
624+
}
625+
626+
tokenBlob, err := iolimits.ReadAtMost(res.Body, iolimits.MaxAuthTokenBodySize)
627+
if err != nil {
628+
return nil, err
629+
}
630+
631+
return newBearerTokenFromJSONBlob(tokenBlob)
632+
}
633+
634+
func (c *dockerClient) getBearerToken(ctx context.Context, challenge challenge,
635+
scopes []authScope) (*bearerToken, error) {
576636
realm, ok := challenge.Parameters["realm"]
577637
if !ok {
578638
return nil, errors.Errorf("missing realm in bearer auth challenge")
579639
}
580640

581-
authReq, err := http.NewRequest("GET", realm, nil)
641+
authReq, err := http.NewRequest(http.MethodGet, realm, nil)
582642
if err != nil {
583643
return nil, err
584644
}
645+
585646
authReq = authReq.WithContext(ctx)
586-
getParams := authReq.URL.Query()
587-
if c.username != "" {
588-
getParams.Add("account", c.username)
647+
params := authReq.URL.Query()
648+
if c.auth.Username != "" {
649+
params.Add("account", c.auth.Username)
589650
}
651+
590652
if service, ok := challenge.Parameters["service"]; ok && service != "" {
591-
getParams.Add("service", service)
653+
params.Add("service", service)
592654
}
655+
593656
for _, scope := range scopes {
594657
if scope.remoteName != "" && scope.actions != "" {
595-
getParams.Add("scope", fmt.Sprintf("repository:%s:%s", scope.remoteName, scope.actions))
658+
params.Add("scope", fmt.Sprintf("repository:%s:%s", scope.remoteName, scope.actions))
596659
}
597660
}
598-
authReq.URL.RawQuery = getParams.Encode()
599-
if c.username != "" && c.password != "" {
600-
authReq.SetBasicAuth(c.username, c.password)
661+
662+
authReq.URL.RawQuery = params.Encode()
663+
664+
if c.auth.Username != "" && c.auth.Password != "" {
665+
authReq.SetBasicAuth(c.auth.Username, c.auth.Password)
601666
}
667+
602668
logrus.Debugf("%s %s", authReq.Method, authReq.URL.String())
603669
res, err := c.client.Do(authReq)
604670
if err != nil {
605671
return nil, err
606672
}
607673
defer res.Body.Close()
608-
switch res.StatusCode {
609-
case http.StatusUnauthorized:
610-
err := clientLib.HandleErrorResponse(res)
611-
logrus.Debugf("Server response when trying to obtain an access token: \n%q", err.Error())
612-
return nil, ErrUnauthorizedForCredentials{Err: err}
613-
case http.StatusOK:
614-
break
615-
default:
616-
return nil, errors.Errorf("unexpected http code: %d (%s), URL: %s", res.StatusCode, http.StatusText(res.StatusCode), authReq.URL)
674+
if err := httpResponseToError(res, "Requesting bear token"); err != nil {
675+
return nil, err
617676
}
618677
tokenBlob, err := iolimits.ReadAtMost(res.Body, iolimits.MaxAuthTokenBodySize)
619678
if err != nil {

pkg/docker/config/config.go

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import (
1818
)
1919

2020
type dockerAuthConfig struct {
21-
Auth string `json:"auth,omitempty"`
21+
Auth string `json:"auth,omitempty"`
22+
IdentityToken string `json:"identitytoken,omitempty"`
2223
}
2324

2425
type dockerConfigFile struct {
@@ -72,20 +73,23 @@ func SetAuthentication(sys *types.SystemContext, registry, username, password st
7273
})
7374
}
7475

75-
// GetAuthentication returns the registry credentials stored in
76-
// either auth.json file or .docker/config.json
77-
// If an entry is not found empty strings are returned for the username and password
78-
func GetAuthentication(sys *types.SystemContext, registry string) (string, string, error) {
76+
// GetCredentials returns the registry credentials stored in either auth.json
77+
// file or .docker/config.json, including support for OAuth2 and IdentityToken.
78+
// If an entry is not found, an empty struct is returned.
79+
func GetCredentials(sys *types.SystemContext, registry string) (types.DockerAuthConfig, error) {
7980
if sys != nil && sys.DockerAuthConfig != nil {
8081
logrus.Debug("Returning credentials from DockerAuthConfig")
81-
return sys.DockerAuthConfig.Username, sys.DockerAuthConfig.Password, nil
82+
return *sys.DockerAuthConfig, nil
8283
}
8384

8485
if enableKeyring {
8586
username, password, err := getAuthFromKernelKeyring(registry)
8687
if err == nil {
8788
logrus.Debug("returning credentials from kernel keyring")
88-
return username, password, nil
89+
return types.DockerAuthConfig{
90+
Username: username,
91+
Password: password,
92+
}, nil
8993
}
9094
}
9195

@@ -104,18 +108,39 @@ func GetAuthentication(sys *types.SystemContext, registry string) (string, strin
104108
authPath{path: filepath.Join(homedir.Get(), dockerLegacyHomePath), legacyFormat: true})
105109

106110
for _, path := range paths {
107-
username, password, err := findAuthentication(registry, path.path, path.legacyFormat)
111+
authConfig, err := findAuthentication(registry, path.path, path.legacyFormat)
108112
if err != nil {
109113
logrus.Debugf("Credentials not found")
110-
return "", "", err
114+
return types.DockerAuthConfig{}, err
111115
}
112-
if username != "" && password != "" {
116+
117+
if (authConfig.Username != "" && authConfig.Password != "") || authConfig.IdentityToken != "" {
113118
logrus.Debugf("Returning credentials from %s", path.path)
114-
return username, password, nil
119+
return authConfig, nil
115120
}
116121
}
122+
117123
logrus.Debugf("Credentials not found")
118-
return "", "", nil
124+
return types.DockerAuthConfig{}, nil
125+
}
126+
127+
// GetAuthentication returns the registry credentials stored in
128+
// either auth.json file or .docker/config.json
129+
// If an entry is not found empty strings are returned for the username and password
130+
//
131+
// Deprecated: This API only has support for username and password. To get the
132+
// support for oauth2 in docker registry authentication, we added the new
133+
// GetCredentials API. The new API should be used and this API is kept to
134+
// maintain backward compatibility.
135+
func GetAuthentication(sys *types.SystemContext, registry string) (string, string, error) {
136+
auth, err := GetCredentials(sys, registry)
137+
if err != nil {
138+
return "", "", err
139+
}
140+
if auth.IdentityToken != "" {
141+
return "", "", errors.Wrap(ErrNotSupported, "non-empty identity token found and this API doesn't support it")
142+
}
143+
return auth.Username, auth.Password, nil
119144
}
120145

121146
// RemoveAuthentication deletes the credentials stored in auth.json
@@ -294,20 +319,28 @@ func deleteAuthFromCredHelper(credHelper, registry string) error {
294319
}
295320

296321
// findAuthentication looks for auth of registry in path
297-
func findAuthentication(registry, path string, legacyFormat bool) (string, string, error) {
322+
func findAuthentication(registry, path string, legacyFormat bool) (types.DockerAuthConfig, error) {
298323
auths, err := readJSONFile(path, legacyFormat)
299324
if err != nil {
300-
return "", "", errors.Wrapf(err, "error reading JSON file %q", path)
325+
return types.DockerAuthConfig{}, errors.Wrapf(err, "error reading JSON file %q", path)
301326
}
302327

303328
// First try cred helpers. They should always be normalized.
304329
if ch, exists := auths.CredHelpers[registry]; exists {
305-
return getAuthFromCredHelper(ch, registry)
330+
username, password, err := getAuthFromCredHelper(ch, registry)
331+
if err != nil {
332+
return types.DockerAuthConfig{}, err
333+
}
334+
335+
return types.DockerAuthConfig{
336+
Username: username,
337+
Password: password,
338+
}, nil
306339
}
307340

308341
// I'm feeling lucky
309342
if val, exists := auths.AuthConfigs[registry]; exists {
310-
return decodeDockerAuth(val.Auth)
343+
return decodeDockerAuth(val)
311344
}
312345

313346
// bad luck; let's normalize the entries first
@@ -316,25 +349,35 @@ func findAuthentication(registry, path string, legacyFormat bool) (string, strin
316349
for k, v := range auths.AuthConfigs {
317350
normalizedAuths[normalizeRegistry(k)] = v
318351
}
352+
319353
if val, exists := normalizedAuths[registry]; exists {
320-
return decodeDockerAuth(val.Auth)
354+
return decodeDockerAuth(val)
321355
}
322-
return "", "", nil
356+
357+
return types.DockerAuthConfig{}, nil
323358
}
324359

325-
func decodeDockerAuth(s string) (string, string, error) {
326-
decoded, err := base64.StdEncoding.DecodeString(s)
360+
// decodeDockerAuth decodes the username and password, which is
361+
// encoded in base64.
362+
func decodeDockerAuth(conf dockerAuthConfig) (types.DockerAuthConfig, error) {
363+
decoded, err := base64.StdEncoding.DecodeString(conf.Auth)
327364
if err != nil {
328-
return "", "", err
365+
return types.DockerAuthConfig{}, err
329366
}
367+
330368
parts := strings.SplitN(string(decoded), ":", 2)
331369
if len(parts) != 2 {
332370
// if it's invalid just skip, as docker does
333-
return "", "", nil
371+
return types.DockerAuthConfig{}, nil
334372
}
373+
335374
user := parts[0]
336375
password := strings.Trim(parts[1], "\x00")
337-
return user, password, nil
376+
return types.DockerAuthConfig{
377+
Username: user,
378+
Password: password,
379+
IdentityToken: conf.IdentityToken,
380+
}, nil
338381
}
339382

340383
// convertToHostname converts a registry url which has http|https prepended

0 commit comments

Comments
 (0)