Skip to content

Commit

Permalink
Merge pull request #829 from yihuaf/dev/yihuaf/authfile
Browse files Browse the repository at this point in the history
Support IdentityToken in registry authn
  • Loading branch information
mtrmac committed Mar 13, 2020
2 parents d1d30d0 + 31d443d commit bf94267
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 138 deletions.
121 changes: 90 additions & 31 deletions docker/docker_client.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package docker

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -97,8 +99,7 @@ type dockerClient struct {
// by detectProperties(). Callers can edit tlsClientConfig.InsecureSkipVerify in the meantime.
tlsClientConfig *tls.Config
// The following members are not set by newDockerClient and must be set by callers if needed.
username string
password string
auth types.DockerAuthConfig
registryToken string
signatureBase signatureStorageBase
scope authScope
Expand Down Expand Up @@ -210,10 +211,11 @@ func dockerCertDir(sys *types.SystemContext, hostPort string) (string, error) {
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
func newDockerClientFromRef(sys *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) {
registry := reference.Domain(ref.ref)
username, password, err := config.GetAuthentication(sys, registry)
auth, err := config.GetCredentials(sys, registry)
if err != nil {
return nil, errors.Wrapf(err, "error getting username and password")
}

sigBase, err := configuredSignatureStorageBase(sys, ref, write)
if err != nil {
return nil, err
Expand All @@ -223,8 +225,7 @@ func newDockerClientFromRef(sys *types.SystemContext, ref dockerReference, write
if err != nil {
return nil, err
}
client.username = username
client.password = password
client.auth = auth
if sys != nil {
client.registryToken = sys.DockerBearerRegistryToken
}
Expand Down Expand Up @@ -289,8 +290,10 @@ func CheckAuth(ctx context.Context, sys *types.SystemContext, username, password
if err != nil {
return errors.Wrapf(err, "error creating new docker client")
}
client.username = username
client.password = password
client.auth = types.DockerAuthConfig{
Username: username,
Password: password,
}

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

// Get credentials from authfile for the underlying hostname
username, password, err := config.GetAuthentication(sys, registry)
auth, err := config.GetCredentials(sys, registry)
if err != nil {
return nil, errors.Wrapf(err, "error getting username and password")
}
Expand All @@ -350,8 +353,7 @@ func SearchRegistry(ctx context.Context, sys *types.SystemContext, registry, ima
if err != nil {
return nil, errors.Wrapf(err, "error creating new docker client")
}
client.username = username
client.password = password
client.auth = auth
if sys != nil {
client.registryToken = sys.DockerBearerRegistryToken
}
Expand Down Expand Up @@ -535,7 +537,7 @@ func (c *dockerClient) setupRequestAuth(req *http.Request, extraScope *authScope
schemeNames = append(schemeNames, challenge.Scheme)
switch challenge.Scheme {
case "basic":
req.SetBasicAuth(c.username, c.password)
req.SetBasicAuth(c.auth.Username, c.auth.Password)
return nil
case "bearer":
registryToken := c.registryToken
Expand All @@ -553,10 +555,19 @@ func (c *dockerClient) setupRequestAuth(req *http.Request, extraScope *authScope
token = t.(bearerToken)
}
if !inCache || time.Now().After(token.expirationTime) {
t, err := c.getBearerToken(req.Context(), challenge, scopes)
var (
t *bearerToken
err error
)
if c.auth.IdentityToken != "" {
t, err = c.getBearerTokenOAuth2(req.Context(), challenge, scopes)
} else {
t, err = c.getBearerToken(req.Context(), challenge, scopes)
}
if err != nil {
return err
}

token = *t
c.tokenCache.Store(cacheKey, token)
}
Expand All @@ -572,48 +583,96 @@ func (c *dockerClient) setupRequestAuth(req *http.Request, extraScope *authScope
return nil
}

func (c *dockerClient) getBearerToken(ctx context.Context, challenge challenge, scopes []authScope) (*bearerToken, error) {
func (c *dockerClient) getBearerTokenOAuth2(ctx context.Context, challenge challenge,
scopes []authScope) (*bearerToken, error) {
realm, ok := challenge.Parameters["realm"]
if !ok {
return nil, errors.Errorf("missing realm in bearer auth challenge")
}

authReq, err := http.NewRequest(http.MethodPost, realm, nil)
if err != nil {
return nil, err
}

authReq = authReq.WithContext(ctx)

// Make the form data required against the oauth2 authentication
// More details here: https://docs.docker.com/registry/spec/auth/oauth/
params := authReq.URL.Query()
if service, ok := challenge.Parameters["service"]; ok && service != "" {
params.Add("service", service)
}
for _, scope := range scopes {
if scope.remoteName != "" && scope.actions != "" {
params.Add("scope", fmt.Sprintf("repository:%s:%s", scope.remoteName, scope.actions))
}
}
params.Add("grant_type", "refresh_token")
params.Add("refresh_token", c.auth.IdentityToken)

authReq.Body = ioutil.NopCloser(bytes.NewBufferString(params.Encode()))
authReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
logrus.Debugf("%s %s", authReq.Method, authReq.URL.String())
res, err := c.client.Do(authReq)
if err != nil {
return nil, err
}
defer res.Body.Close()
if err := httpResponseToError(res, "Trying to obtain access token"); err != nil {
return nil, err
}

tokenBlob, err := iolimits.ReadAtMost(res.Body, iolimits.MaxAuthTokenBodySize)
if err != nil {
return nil, err
}

return newBearerTokenFromJSONBlob(tokenBlob)
}

func (c *dockerClient) getBearerToken(ctx context.Context, challenge challenge,
scopes []authScope) (*bearerToken, error) {
realm, ok := challenge.Parameters["realm"]
if !ok {
return nil, errors.Errorf("missing realm in bearer auth challenge")
}

authReq, err := http.NewRequest("GET", realm, nil)
authReq, err := http.NewRequest(http.MethodGet, realm, nil)
if err != nil {
return nil, err
}

authReq = authReq.WithContext(ctx)
getParams := authReq.URL.Query()
if c.username != "" {
getParams.Add("account", c.username)
params := authReq.URL.Query()
if c.auth.Username != "" {
params.Add("account", c.auth.Username)
}

if service, ok := challenge.Parameters["service"]; ok && service != "" {
getParams.Add("service", service)
params.Add("service", service)
}

for _, scope := range scopes {
if scope.remoteName != "" && scope.actions != "" {
getParams.Add("scope", fmt.Sprintf("repository:%s:%s", scope.remoteName, scope.actions))
params.Add("scope", fmt.Sprintf("repository:%s:%s", scope.remoteName, scope.actions))
}
}
authReq.URL.RawQuery = getParams.Encode()
if c.username != "" && c.password != "" {
authReq.SetBasicAuth(c.username, c.password)

authReq.URL.RawQuery = params.Encode()

if c.auth.Username != "" && c.auth.Password != "" {
authReq.SetBasicAuth(c.auth.Username, c.auth.Password)
}

logrus.Debugf("%s %s", authReq.Method, authReq.URL.String())
res, err := c.client.Do(authReq)
if err != nil {
return nil, err
}
defer res.Body.Close()
switch res.StatusCode {
case http.StatusUnauthorized:
err := clientLib.HandleErrorResponse(res)
logrus.Debugf("Server response when trying to obtain an access token: \n%q", err.Error())
return nil, ErrUnauthorizedForCredentials{Err: err}
case http.StatusOK:
break
default:
return nil, errors.Errorf("unexpected http code: %d (%s), URL: %s", res.StatusCode, http.StatusText(res.StatusCode), authReq.URL)
if err := httpResponseToError(res, "Requesting bear token"); err != nil {
return nil, err
}
tokenBlob, err := iolimits.ReadAtMost(res.Body, iolimits.MaxAuthTokenBodySize)
if err != nil {
Expand Down
89 changes: 66 additions & 23 deletions pkg/docker/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import (
)

type dockerAuthConfig struct {
Auth string `json:"auth,omitempty"`
Auth string `json:"auth,omitempty"`
IdentityToken string `json:"identitytoken,omitempty"`
}

type dockerConfigFile struct {
Expand Down Expand Up @@ -72,20 +73,23 @@ func SetAuthentication(sys *types.SystemContext, registry, username, password st
})
}

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

if enableKeyring {
username, password, err := getAuthFromKernelKeyring(registry)
if err == nil {
logrus.Debug("returning credentials from kernel keyring")
return username, password, nil
return types.DockerAuthConfig{
Username: username,
Password: password,
}, nil
}
}

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

for _, path := range paths {
username, password, err := findAuthentication(registry, path.path, path.legacyFormat)
authConfig, err := findAuthentication(registry, path.path, path.legacyFormat)
if err != nil {
logrus.Debugf("Credentials not found")
return "", "", err
return types.DockerAuthConfig{}, err
}
if username != "" && password != "" {

if (authConfig.Username != "" && authConfig.Password != "") || authConfig.IdentityToken != "" {
logrus.Debugf("Returning credentials from %s", path.path)
return username, password, nil
return authConfig, nil
}
}

logrus.Debugf("Credentials not found")
return "", "", nil
return types.DockerAuthConfig{}, nil
}

// GetAuthentication returns the registry credentials stored in
// either auth.json file or .docker/config.json
// If an entry is not found empty strings are returned for the username and password
//
// Deprecated: This API only has support for username and password. To get the
// support for oauth2 in docker registry authentication, we added the new
// GetCredentials API. The new API should be used and this API is kept to
// maintain backward compatibility.
func GetAuthentication(sys *types.SystemContext, registry string) (string, string, error) {
auth, err := GetCredentials(sys, registry)
if err != nil {
return "", "", err
}
if auth.IdentityToken != "" {
return "", "", errors.Wrap(ErrNotSupported, "non-empty identity token found and this API doesn't support it")
}
return auth.Username, auth.Password, nil
}

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

// findAuthentication looks for auth of registry in path
func findAuthentication(registry, path string, legacyFormat bool) (string, string, error) {
func findAuthentication(registry, path string, legacyFormat bool) (types.DockerAuthConfig, error) {
auths, err := readJSONFile(path, legacyFormat)
if err != nil {
return "", "", errors.Wrapf(err, "error reading JSON file %q", path)
return types.DockerAuthConfig{}, errors.Wrapf(err, "error reading JSON file %q", path)
}

// First try cred helpers. They should always be normalized.
if ch, exists := auths.CredHelpers[registry]; exists {
return getAuthFromCredHelper(ch, registry)
username, password, err := getAuthFromCredHelper(ch, registry)
if err != nil {
return types.DockerAuthConfig{}, err
}

return types.DockerAuthConfig{
Username: username,
Password: password,
}, nil
}

// I'm feeling lucky
if val, exists := auths.AuthConfigs[registry]; exists {
return decodeDockerAuth(val.Auth)
return decodeDockerAuth(val)
}

// bad luck; let's normalize the entries first
Expand All @@ -316,25 +349,35 @@ func findAuthentication(registry, path string, legacyFormat bool) (string, strin
for k, v := range auths.AuthConfigs {
normalizedAuths[normalizeRegistry(k)] = v
}

if val, exists := normalizedAuths[registry]; exists {
return decodeDockerAuth(val.Auth)
return decodeDockerAuth(val)
}
return "", "", nil

return types.DockerAuthConfig{}, nil
}

func decodeDockerAuth(s string) (string, string, error) {
decoded, err := base64.StdEncoding.DecodeString(s)
// decodeDockerAuth decodes the username and password, which is
// encoded in base64.
func decodeDockerAuth(conf dockerAuthConfig) (types.DockerAuthConfig, error) {
decoded, err := base64.StdEncoding.DecodeString(conf.Auth)
if err != nil {
return "", "", err
return types.DockerAuthConfig{}, err
}

parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
// if it's invalid just skip, as docker does
return "", "", nil
return types.DockerAuthConfig{}, nil
}

user := parts[0]
password := strings.Trim(parts[1], "\x00")
return user, password, nil
return types.DockerAuthConfig{
Username: user,
Password: password,
IdentityToken: conf.IdentityToken,
}, nil
}

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

0 comments on commit bf94267

Please sign in to comment.