This change allows API clients to retrieve an authentication token from
a registry, and then pass that token directly to the API.
Example usage:
REPO_USER=dhiltgen
read -s PASSWORD
REPO=privateorg/repo
AUTH_URL=https://auth.docker.io/token
TOKEN=$(curl -s -u "${REPO_USER}:${PASSWORD}" "${AUTH_URL}?scope=repository:${REPO}:pull&service=registry.docker.io" |
jq -r ".token")
HEADER=$(echo "{\"registrytoken\":\"${TOKEN}\"}"|base64 -w 0 )
curl -s -D - -H "X-Registry-Auth: ${HEADER}" -X POST "http://localhost:2376/images/create?fromImage=${REPO}"
Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
| ... | ... |
@@ -51,6 +51,7 @@ type AuthConfig struct {
|
| 51 | 51 |
Auth string `json:"auth"` |
| 52 | 52 |
Email string `json:"email"` |
| 53 | 53 |
ServerAddress string `json:"serveraddress,omitempty"` |
| 54 |
+ RegistryToken string `json:"registrytoken,omitempty"` |
|
| 54 | 55 |
} |
| 55 | 56 |
|
| 56 | 57 |
// ConfigFile ~/.docker/config.json file info |
| ... | ... |
@@ -2,6 +2,7 @@ package distribution |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"errors" |
| 5 |
+ "fmt" |
|
| 5 | 6 |
"net" |
| 6 | 7 |
"net/http" |
| 7 | 8 |
"net/url" |
| ... | ... |
@@ -91,10 +92,15 @@ func NewV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEnd |
| 91 | 91 |
return nil, err |
| 92 | 92 |
} |
| 93 | 93 |
|
| 94 |
- creds := dumbCredentialStore{auth: authConfig}
|
|
| 95 |
- tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName.Name(), actions...) |
|
| 96 |
- basicHandler := auth.NewBasicHandler(creds) |
|
| 97 |
- modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) |
|
| 94 |
+ if authConfig.RegistryToken != "" {
|
|
| 95 |
+ passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken}
|
|
| 96 |
+ modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler)) |
|
| 97 |
+ } else {
|
|
| 98 |
+ creds := dumbCredentialStore{auth: authConfig}
|
|
| 99 |
+ tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName.Name(), actions...) |
|
| 100 |
+ basicHandler := auth.NewBasicHandler(creds) |
|
| 101 |
+ modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) |
|
| 102 |
+ } |
|
| 98 | 103 |
tr := transport.NewTransport(base, modifiers...) |
| 99 | 104 |
|
| 100 | 105 |
return client.NewRepository(ctx, repoName.Name(), endpoint.URL, tr) |
| ... | ... |
@@ -113,3 +119,16 @@ func digestFromManifest(m *schema1.SignedManifest, localName string) (digest.Dig |
| 113 | 113 |
} |
| 114 | 114 |
return manifestDigest, len(payload), nil |
| 115 | 115 |
} |
| 116 |
+ |
|
| 117 |
+type existingTokenHandler struct {
|
|
| 118 |
+ token string |
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+func (th *existingTokenHandler) Scheme() string {
|
|
| 122 |
+ return "bearer" |
|
| 123 |
+} |
|
| 124 |
+ |
|
| 125 |
+func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
|
|
| 126 |
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.token))
|
|
| 127 |
+ return nil |
|
| 128 |
+} |
| 116 | 129 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,95 @@ |
| 0 |
+package distribution |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "net/http" |
|
| 4 |
+ "net/http/httptest" |
|
| 5 |
+ "os" |
|
| 6 |
+ "strings" |
|
| 7 |
+ "testing" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/Sirupsen/logrus" |
|
| 10 |
+ "github.com/docker/distribution/reference" |
|
| 11 |
+ "github.com/docker/distribution/registry/client/auth" |
|
| 12 |
+ "github.com/docker/docker/cliconfig" |
|
| 13 |
+ "github.com/docker/docker/pkg/streamformatter" |
|
| 14 |
+ "github.com/docker/docker/registry" |
|
| 15 |
+ "github.com/docker/docker/utils" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+func TestTokenPassThru(t *testing.T) {
|
|
| 19 |
+ authConfig := &cliconfig.AuthConfig{
|
|
| 20 |
+ RegistryToken: "mysecrettoken", |
|
| 21 |
+ } |
|
| 22 |
+ gotToken := false |
|
| 23 |
+ handler := func(w http.ResponseWriter, r *http.Request) {
|
|
| 24 |
+ if strings.Contains(r.Header.Get("Authorization"), authConfig.RegistryToken) {
|
|
| 25 |
+ logrus.Debug("Detected registry token in auth header")
|
|
| 26 |
+ gotToken = true |
|
| 27 |
+ } |
|
| 28 |
+ if r.RequestURI == "/v2/" {
|
|
| 29 |
+ w.Header().Set("WWW-Authenticate", `Bearer realm="foorealm"`)
|
|
| 30 |
+ w.WriteHeader(401) |
|
| 31 |
+ } |
|
| 32 |
+ } |
|
| 33 |
+ ts := httptest.NewServer(http.HandlerFunc(handler)) |
|
| 34 |
+ defer ts.Close() |
|
| 35 |
+ |
|
| 36 |
+ tmp, err := utils.TestDirectory("")
|
|
| 37 |
+ if err != nil {
|
|
| 38 |
+ t.Fatal(err) |
|
| 39 |
+ } |
|
| 40 |
+ defer os.RemoveAll(tmp) |
|
| 41 |
+ |
|
| 42 |
+ endpoint := registry.APIEndpoint{
|
|
| 43 |
+ Mirror: false, |
|
| 44 |
+ URL: ts.URL, |
|
| 45 |
+ Version: 2, |
|
| 46 |
+ Official: false, |
|
| 47 |
+ TrimHostname: false, |
|
| 48 |
+ TLSConfig: nil, |
|
| 49 |
+ //VersionHeader: "verheader", |
|
| 50 |
+ Versions: []auth.APIVersion{
|
|
| 51 |
+ {
|
|
| 52 |
+ Type: "registry", |
|
| 53 |
+ Version: "2", |
|
| 54 |
+ }, |
|
| 55 |
+ }, |
|
| 56 |
+ } |
|
| 57 |
+ n, _ := reference.ParseNamed("testremotename")
|
|
| 58 |
+ repoInfo := ®istry.RepositoryInfo{
|
|
| 59 |
+ Index: ®istry.IndexInfo{
|
|
| 60 |
+ Name: "testrepo", |
|
| 61 |
+ Mirrors: nil, |
|
| 62 |
+ Secure: false, |
|
| 63 |
+ Official: false, |
|
| 64 |
+ }, |
|
| 65 |
+ RemoteName: n, |
|
| 66 |
+ LocalName: n, |
|
| 67 |
+ CanonicalName: n, |
|
| 68 |
+ Official: false, |
|
| 69 |
+ } |
|
| 70 |
+ imagePullConfig := &ImagePullConfig{
|
|
| 71 |
+ MetaHeaders: http.Header{},
|
|
| 72 |
+ AuthConfig: authConfig, |
|
| 73 |
+ } |
|
| 74 |
+ sf := streamformatter.NewJSONStreamFormatter() |
|
| 75 |
+ puller, err := newPuller(endpoint, repoInfo, imagePullConfig, sf) |
|
| 76 |
+ if err != nil {
|
|
| 77 |
+ t.Fatal(err) |
|
| 78 |
+ } |
|
| 79 |
+ p := puller.(*v2Puller) |
|
| 80 |
+ p.repo, err = NewV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull") |
|
| 81 |
+ if err != nil {
|
|
| 82 |
+ t.Fatal(err) |
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ logrus.Debug("About to pull")
|
|
| 86 |
+ // We expect it to fail, since we haven't mock'd the full registry exchange in our handler above |
|
| 87 |
+ tag, _ := reference.WithTag(n, "tag_goes_here") |
|
| 88 |
+ _ = p.pullV2Repository(tag) |
|
| 89 |
+ |
|
| 90 |
+ if !gotToken {
|
|
| 91 |
+ t.Fatal("Failed to receive registry token")
|
|
| 92 |
+ } |
|
| 93 |
+ |
|
| 94 |
+} |
| ... | ... |
@@ -101,6 +101,7 @@ This section lists each version from latest to oldest. Each listing includes a |
| 101 | 101 |
* `GET /networks/(name)` now returns a `Name` field for each container attached to the network. |
| 102 | 102 |
* `GET /version` now returns the `BuildTime` field in RFC3339Nano format to make it |
| 103 | 103 |
consistent with other date/time values returned by the API. |
| 104 |
+* `AuthConfig` now supports a `registrytoken` for token based authentication |
|
| 104 | 105 |
|
| 105 | 106 |
### v1.21 API changes |
| 106 | 107 |
|
| ... | ... |
@@ -1532,7 +1532,24 @@ Query Parameters: |
| 1532 | 1532 |
|
| 1533 | 1533 |
Request Headers: |
| 1534 | 1534 |
|
| 1535 |
-- **X-Registry-Auth** – base64-encoded AuthConfig object |
|
| 1535 |
+- **X-Registry-Auth** – base64-encoded AuthConfig object, containing either login information, or a token |
|
| 1536 |
+ - Credential based login: |
|
| 1537 |
+ |
|
| 1538 |
+ ``` |
|
| 1539 |
+ {
|
|
| 1540 |
+ "username": "jdoe", |
|
| 1541 |
+ "password": "secret", |
|
| 1542 |
+ "email": "jdoe@acme.com", |
|
| 1543 |
+ } |
|
| 1544 |
+ ``` |
|
| 1545 |
+ |
|
| 1546 |
+ - Token based login: |
|
| 1547 |
+ |
|
| 1548 |
+ ``` |
|
| 1549 |
+ {
|
|
| 1550 |
+ "registrytoken": "9cbaf023786cd7..." |
|
| 1551 |
+ } |
|
| 1552 |
+ ``` |
|
| 1536 | 1553 |
|
| 1537 | 1554 |
Status Codes: |
| 1538 | 1555 |
|
| ... | ... |
@@ -1741,8 +1758,24 @@ Query Parameters: |
| 1741 | 1741 |
|
| 1742 | 1742 |
Request Headers: |
| 1743 | 1743 |
|
| 1744 |
-- **X-Registry-Auth** – Include a base64-encoded AuthConfig. |
|
| 1745 |
- object. |
|
| 1744 |
+- **X-Registry-Auth** – base64-encoded AuthConfig object, containing either login information, or a token |
|
| 1745 |
+ - Credential based login: |
|
| 1746 |
+ |
|
| 1747 |
+ ``` |
|
| 1748 |
+ {
|
|
| 1749 |
+ "username": "jdoe", |
|
| 1750 |
+ "password": "secret", |
|
| 1751 |
+ "email": "jdoe@acme.com", |
|
| 1752 |
+ } |
|
| 1753 |
+ ``` |
|
| 1754 |
+ |
|
| 1755 |
+ - Token based login: |
|
| 1756 |
+ |
|
| 1757 |
+ ``` |
|
| 1758 |
+ {
|
|
| 1759 |
+ "registrytoken": "9cbaf023786cd7..." |
|
| 1760 |
+ } |
|
| 1761 |
+ ``` |
|
| 1746 | 1762 |
|
| 1747 | 1763 |
Status Codes: |
| 1748 | 1764 |
|