Repository: deislabs/ratify (go)

Issue:
Docker config auth provider should work with identity tokens
## Problem

The ORAS store currently supports the [Docker config file](https://github.com/deislabs/ratify/blob/main/docs/reference/oras-auth-provider.md#1-docker-config) as an authentication provider, which is useful for development and standalone usage of Ratify (ex: in a CI pipeline)

The Docker config file format supports [multiple types](https://github.com/docker/cli/blob/master/cli/config/types/authconfig.go) of credentials, including `password` (already supported [here](https://github.com/deislabs/ratify/blob/9c682011a0c5da32d986b748b1e66541344ba843/pkg/referrerstore/oras/authprovider/authprovider.go#L126-L131)) and `identitytoken`, which does not work

This leads to a poor experience when used with tools that set `identitytoken`, ex: `az acr login`. Currently I have to introduce hacky conversion commands so that Ratify picks up the `password` property for use with ORAS:

```shell
# login to my registry
az acr login -n $MYREGISTRY

# hack to convert identitytoken -> password
cat <<< $(jq --arg registry $REGISTRY '.auths[$registry] |= {"username":"00000000-0000-0000-0000-000000000000", "password": .identitytoken }' <~/.docker/config.json) > ~/.docker/config.json

# verify some assets
ratify verify $MYREGISTRY.azurecr.io/myimage:sometag
```

## Proposed solution

It would be great if Ratify supported `identitytoken` so that I could create a pipeline with the following flow, without having to jump through the above hoops to get authenticated

```shell
az acr login -n $MYREGISTRY
ratify verify $MYREGISTRY.azurecr.io/myimage:sometag
```

Working proof of concept here: https://github.com/cse-labs/ratify/commit/47967b7b76f675197726258562802c0ec5560a53

Pull request:
Add Docker identity token support for ORAS store
# Description

Per https://github.com/deislabs/ratify/issues/449, this PR adds support for `identitytoken` with Docker config

Fixes #449

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)

# How Has This Been Tested?

- [x] Auth provider unit tests
- [x] Manually tested via

```shell
az acr login -n myregistry
ratify verify -s myregistry.azurecr.io/myimage:tag
```

Previously, this step would fail because references could not be enumerated. After this PR, the above works as expected

# Checklist:

- [x] Does the affected code have corresponding tests?
- [ ] Are the changes documented, not just with inline documentation, but also with conceptual documentation such as an overview of a new feature, or task-based documentation like a tutorial? Consider if this change should be announced on your project blog.
- [ ] Does this introduce breaking changes that would require an announcement or bumping the major version?
- [ ] Do all new files have appropriate license header?


Patch:
diff --git a/pkg/referrerstore/oras/authprovider/authprovider.go b/pkg/referrerstore/oras/authprovider/authprovider.go
index 3603ba5..332bed9 100644
--- a/pkg/referrerstore/oras/authprovider/authprovider.go
+++ b/pkg/referrerstore/oras/authprovider/authprovider.go
@@ -34,6 +34,7 @@ import (
 type AuthConfig struct {
 	Username  string
 	Password  string
+	IdentityToken string
 	Email     string
 	Provider  AuthProvider
 	ExpiresOn time.Time
@@ -127,6 +128,7 @@ func (d *defaultAuthProvider) Provide(ctx context.Context, artifact string) (Aut
 	authConfig := AuthConfig{
 		Username: dockerAuthConfig.Username,
 		Password: dockerAuthConfig.Password,
+		IdentityToken: dockerAuthConfig.IdentityToken,
 		Provider: d,
 	}
 
diff --git a/pkg/referrerstore/oras/authprovider/k8secret_authprovider.go b/pkg/referrerstore/oras/authprovider/k8secret_authprovider.go
index 3b76b57..1dcdd6c 100644
--- a/pkg/referrerstore/oras/authprovider/k8secret_authprovider.go
+++ b/pkg/referrerstore/oras/authprovider/k8secret_authprovider.go
@@ -211,6 +211,7 @@ func (d *k8SecretAuthProvider) resolveCredentialFromSecret(hostName string, secr
 	return AuthConfig{
 		Username:  authConfig.Username,
 		Password:  authConfig.Password,
+		IdentityToken: authConfig.IdentityToken,
 		Provider:  d,
 		ExpiresOn: time.Now().Add(secretTimeout),
 	}, nil
diff --git a/pkg/referrerstore/oras/oras.go b/pkg/referrerstore/oras/oras.go
index b8b0d38..192be5d 100644
--- a/pkg/referrerstore/oras/oras.go
+++ b/pkg/referrerstore/oras/oras.go
@@ -336,10 +336,11 @@ func (store *orasStore) createRepository(ctx context.Context, targetRef common.R
 
 	// set the provider to return the resolved credentials
 	credentialProvider := func(ctx context.Context, registry string) (auth.Credential, error) {
-		if authConfig.Username != "" || authConfig.Password != "" {
+		if authConfig.Username != "" || authConfig.Password != "" || authConfig.IdentityToken != "" {
 			return auth.Credential{
 				Username: authConfig.Username,
 				Password: authConfig.Password,
+				RefreshToken: authConfig.IdentityToken,
 			}, nil
 		}
 		return auth.EmptyCredential, nil


Test patch:
diff --git a/pkg/referrerstore/oras/authprovider/authprovider_test.go b/pkg/referrerstore/oras/authprovider/authprovider_test.go
index 32f301e..d7efc7b 100644
--- a/pkg/referrerstore/oras/authprovider/authprovider_test.go
+++ b/pkg/referrerstore/oras/authprovider/authprovider_test.go
@@ -72,3 +72,39 @@ func TestProvide_ExternalDockerConfigPath_ExpectedResults(t *testing.T) {
 		t.Fatalf("incorrect username %v or password %v returned", authConfig.Username, authConfig.Password)
 	}
 }
+
+func TestProvide_ExternalDockerConfigPathWithIdentityToken_ExpectedResults(t *testing.T) {
+	tmpHome, err := ioutil.TempDir("", "config-test")
+	if err != nil {
+		t.Fatalf("unexpected error when creating temporary directory: %v", err)
+	}
+	defer os.RemoveAll(tmpHome)
+
+	fn := filepath.Join(tmpHome, "config.json")
+	js := `{
+		"auths": {
+			"index.docker.io": {
+				"auth": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwOg==",
+				"identitytoken": "OPAQUE_TOKEN"
+			}
+		}
+	}`
+
+	err = ioutil.WriteFile(fn, []byte(js), 0600)
+	if err != nil {
+		t.Fatalf("unexpected error when writing config file: %v", err)
+	}
+
+	defaultProvider := defaultAuthProvider{
+		configPath: fn,
+	}
+
+	authConfig, err := defaultProvider.Provide(context.Background(), "index.docker.io/v1/test:v1")
+	if err != nil {
+		t.Fatalf("unexpected error in Provide: %v", err)
+	}
+
+	if authConfig.Username != "00000000-0000-0000-0000-000000000000" || authConfig.IdentityToken != "OPAQUE_TOKEN" {
+		t.Fatalf("incorrect username %v or identitytoken %v returned", authConfig.Username, authConfig.IdentityToken)
+	}
+}
diff --git a/pkg/referrerstore/oras/authprovider/k8secret_authprovider_test.go b/pkg/referrerstore/oras/authprovider/k8secret_authprovider_test.go
index 9a961dc..84a6f60 100644
--- a/pkg/referrerstore/oras/authprovider/k8secret_authprovider_test.go
+++ b/pkg/referrerstore/oras/authprovider/k8secret_authprovider_test.go
@@ -48,6 +48,32 @@ func TestProvide_K8SecretDockerConfigJson_ReturnsExpected(t *testing.T) {
 	}
 }
 
+func TestProvide_K8SecretDockerConfigJsonWithIdentityToken_ReturnsExpected(t *testing.T) {
+	var testSecret core.Secret
+	js := `{
+		"auths": {
+			"index.docker.io": {
+				"auth": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwOg==",
+				"identitytoken": "OPAQUE_TOKEN"
+			}
+		}
+	}`
+	testSecret.Data = make(map[string][]byte)
+	testSecret.Data[core.DockerConfigJsonKey] = []byte(js)
+	testSecret.Type = core.SecretTypeDockerConfigJson
+
+	var k8secretprovider k8SecretAuthProvider
+
+	authConfig, err := k8secretprovider.resolveCredentialFromSecret("index.docker.io", &testSecret)
+	if err != nil {
+		t.Fatalf("resolveCredentialFromSecret failed to get credential with err %v", err)
+	}
+
+	if authConfig.Username != "00000000-0000-0000-0000-000000000000" || authConfig.IdentityToken != "OPAQUE_TOKEN" {
+		t.Fatalf("resolveCredentialFromSecret returned incorrect credentials (username: %s, identitytoken: %s)", authConfig.Username, authConfig.IdentityToken)
+	}
+}
+
 // Checks K8 DockerCfg Secret is properly extracted and
 // credentials returned when Provide is called
 func TestProvide_K8SecretDockerCfg_ReturnsExpected(t *testing.T) {
@@ -73,6 +99,30 @@ func TestProvide_K8SecretDockerCfg_ReturnsExpected(t *testing.T) {
 	}
 }
 
+func TestProvide_K8SecretDockerCfgWithIdentityToken_ReturnsExpected(t *testing.T) {
+	var testSecret core.Secret
+	js := `{
+		"index.docker.io": {
+			"auth": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwOg==",
+			"identitytoken": "OPAQUE_TOKEN"
+		}
+	}`
+	testSecret.Data = make(map[string][]byte)
+	testSecret.Data[core.DockerConfigKey] = []byte(js)
+	testSecret.Type = core.SecretTypeDockercfg
+
+	var k8secretprovider k8SecretAuthProvider
+
+	authConfig, err := k8secretprovider.resolveCredentialFromSecret("index.docker.io", &testSecret)
+	if err != nil {
+		t.Fatalf("resolveCredentialFromSecret failed to get credential with err %v", err)
+	}
+
+	if authConfig.Username != "00000000-0000-0000-0000-000000000000" || authConfig.IdentityToken != "OPAQUE_TOKEN" {
+		t.Fatalf("resolveCredentialFromSecret returned incorrect credentials (username: %s, identitytoken: %s)", authConfig.Username, authConfig.IdentityToken)
+	}
+}
+
 // Checks an error is returned for non-existent registry credential
 func TestProvide_K8SecretNonExistentRegistry_ReturnsExpected(t *testing.T) {
 	var testSecret core.Secret