From 869c73d0ad6a2321b0a69205c0e3c139183bd376 Mon Sep 17 00:00:00 2001
From: Soule BA <soule@weave.works>
Date: Thu, 1 Sep 2022 13:37:41 +0200
Subject: [PATCH] secretRef take precedence over provider

if secretRef is provided, we do not attempt to resolve oidc

Signed-off-by: Soule BA <soule@weave.works>
---
 controllers/helmchart_controller.go           |  12 +-
 controllers/helmchart_controller_test.go      | 230 +++++++++++++++++-
 controllers/helmrepository_controller_oci.go  |  44 +---
 .../helmrepository_controller_oci_test.go     | 149 ++++++++++++
 controllers/ocirepository_controller.go       |  20 +-
 controllers/ocirepository_controller_test.go  |  42 ++++
 controllers/suite_test.go                     |  11 +-
 internal/util/auth.go                         |  30 +++
 8 files changed, 468 insertions(+), 70 deletions(-)
 create mode 100644 internal/util/auth.go

diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go
index 24650f5e0..965ddcedc 100644
--- a/controllers/helmchart_controller.go
+++ b/controllers/helmchart_controller.go
@@ -516,10 +516,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
 		}
 
 		loginOpts = append([]helmreg.LoginOption{}, loginOpt)
-	}
-
-	if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
-		auth, authErr := oidcAuth(ctxTimeout, repo)
+	} else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
+		auth, authErr := oidcAuthFromAdapter(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
 		if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
 			e := &serror.Event{
 				Err:    fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr),
@@ -991,10 +989,8 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
 			}
 
 			loginOpts = append([]helmreg.LoginOption{}, loginOpt)
-		}
-
-		if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
-			auth, authErr := oidcAuth(ctxTimeout, repo)
+		} else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
+			auth, authErr := oidcAuthFromAdapter(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
 			if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
 				return nil, fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr)
 			}
diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go
index e9c3920d2..631286bc1 100644
--- a/controllers/helmchart_controller_test.go
+++ b/controllers/helmchart_controller_test.go
@@ -44,6 +44,7 @@ import (
 	kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
 	"github.com/fluxcd/pkg/apis/meta"
@@ -893,21 +894,11 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
 		chartPath = "testdata/charts/helmchart-0.1.0.tgz"
 	)
 
-	// Login to the registry
-	err := testRegistryServer.registryClient.Login(testRegistryServer.registryHost,
-		helmreg.LoginOptBasicAuth(testRegistryUsername, testRegistryPassword),
-		helmreg.LoginOptInsecure(true))
-	g.Expect(err).NotTo(HaveOccurred())
-
 	// Load a test chart
 	chartData, err := ioutil.ReadFile(chartPath)
-	g.Expect(err).NotTo(HaveOccurred())
-	metadata, err := extractChartMeta(chartData)
-	g.Expect(err).NotTo(HaveOccurred())
 
 	// Upload the test chart
-	ref := fmt.Sprintf("%s/testrepo/%s:%s", testRegistryServer.registryHost, metadata.Name, metadata.Version)
-	_, err = testRegistryServer.registryClient.Push(chartData, ref)
+	metadata, err := loadTestChartToOCI(chartData, chartPath, testRegistryServer)
 	g.Expect(err).NotTo(HaveOccurred())
 
 	storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
@@ -2038,6 +2029,194 @@ func TestHelmChartReconciler_notify(t *testing.T) {
 	}
 }
 
+func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
+	const (
+		chartPath = "testdata/charts/helmchart-0.1.0.tgz"
+	)
+
+	type secretOptions struct {
+		username string
+		password string
+	}
+
+	tests := []struct {
+		name             string
+		url              string
+		registryOpts     registryOptions
+		secretOpts       secretOptions
+		provider         string
+		providerImg      string
+		want             sreconcile.Result
+		wantErr          bool
+		assertConditions []metav1.Condition
+	}{
+		{
+			name: "HTTP without basic auth",
+			want: sreconcile.ResultSuccess,
+			assertConditions: []metav1.Condition{
+				*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled '<helmchart>' chart with version '<version>'"),
+			},
+		},
+		{
+			name: "HTTP with basic auth secret",
+			want: sreconcile.ResultSuccess,
+			registryOpts: registryOptions{
+				withBasicAuth: true,
+			},
+			secretOpts: secretOptions{
+				username: testRegistryUsername,
+				password: testRegistryPassword,
+			},
+			assertConditions: []metav1.Condition{
+				*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled '<helmchart>' chart with version '<version>'"),
+			},
+		},
+		{
+			name:    "HTTP registry - basic auth with invalid secret",
+			want:    sreconcile.ResultEmpty,
+			wantErr: true,
+			registryOpts: registryOptions{
+				withBasicAuth: true,
+			},
+			secretOpts: secretOptions{
+				username: "wrong-pass",
+				password: "wrong-pass",
+			},
+			assertConditions: []metav1.Condition{
+				*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to login to OCI registry"),
+			},
+		},
+		{
+			name:        "with contextual login provider",
+			wantErr:     true,
+			provider:    "aws",
+			providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test",
+			assertConditions: []metav1.Condition{
+				*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to get credential from"),
+			},
+		},
+		{
+			name: "with contextual login provider and secretRef",
+			want: sreconcile.ResultSuccess,
+			registryOpts: registryOptions{
+				withBasicAuth: true,
+			},
+			secretOpts: secretOptions{
+				username: testRegistryUsername,
+				password: testRegistryPassword,
+			},
+			provider: "azure",
+			assertConditions: []metav1.Condition{
+				*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled '<helmchart>' chart with version '<version>'"),
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme())
+			workspaceDir := t.TempDir()
+			server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts)
+
+			g.Expect(err).NotTo(HaveOccurred())
+
+			// Load a test chart
+			chartData, err := ioutil.ReadFile(chartPath)
+
+			// Upload the test chart
+			metadata, err := loadTestChartToOCI(chartData, chartPath, server)
+			g.Expect(err).NotTo(HaveOccurred())
+			g.Expect(err).ToNot(HaveOccurred())
+
+			repo := &sourcev1.HelmRepository{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "auth-strategy-",
+				},
+				Spec: sourcev1.HelmRepositorySpec{
+					Interval: metav1.Duration{Duration: interval},
+					Timeout:  &metav1.Duration{Duration: timeout},
+					Type:     sourcev1.HelmRepositoryTypeOCI,
+					Provider: sourcev1.GenericOCIProvider,
+					URL:      fmt.Sprintf("oci://%s/testrepo", server.registryHost),
+				},
+			}
+
+			if tt.provider != "" {
+				repo.Spec.Provider = tt.provider
+			}
+			// If a provider specific image is provided, overwrite existing URL
+			// set earlier. It'll fail but it's necessary to set them because
+			// the login check expects the URLs to be of certain pattern.
+			if tt.providerImg != "" {
+				repo.Spec.URL = tt.providerImg
+			}
+
+			if tt.secretOpts.username != "" && tt.secretOpts.password != "" {
+				secret := &corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "auth-secretref",
+					},
+					Type: corev1.SecretTypeDockerConfigJson,
+					Data: map[string][]byte{
+						".dockerconfigjson": []byte(fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`,
+							server.registryHost, tt.secretOpts.username, tt.secretOpts.password)),
+					},
+				}
+
+				repo.Spec.SecretRef = &meta.LocalObjectReference{
+					Name: secret.Name,
+				}
+				builder.WithObjects(secret, repo)
+			} else {
+				builder.WithObjects(repo)
+			}
+
+			obj := &sourcev1.HelmChart{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "auth-strategy-",
+				},
+				Spec: sourcev1.HelmChartSpec{
+					Chart:   metadata.Name,
+					Version: metadata.Version,
+					SourceRef: sourcev1.LocalHelmChartSourceReference{
+						Kind: sourcev1.HelmRepositoryKind,
+						Name: repo.Name,
+					},
+					Interval: metav1.Duration{Duration: interval},
+				},
+			}
+
+			r := &HelmChartReconciler{
+				Client:                  builder.Build(),
+				EventRecorder:           record.NewFakeRecorder(32),
+				Getters:                 testGetters,
+				RegistryClientGenerator: registry.ClientGenerator,
+			}
+
+			var b chart.Build
+			defer func() {
+				if _, err := os.Stat(b.Path); !os.IsNotExist(err) {
+					err := os.Remove(b.Path)
+					g.Expect(err).NotTo(HaveOccurred())
+				}
+			}()
+
+			assertConditions := tt.assertConditions
+			for k := range assertConditions {
+				assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<helmchart>", metadata.Name)
+				assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<version>", metadata.Version)
+			}
+
+			got, err := r.reconcileSource(ctx, obj, &b)
+			g.Expect(err != nil).To(Equal(tt.wantErr))
+			g.Expect(got).To(Equal(tt.want))
+			g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
+		})
+	}
+}
+
 // extractChartMeta is used to extract a chart metadata from a byte array
 func extractChartMeta(chartData []byte) (*hchart.Metadata, error) {
 	ch, err := loader.LoadArchive(bytes.NewReader(chartData))
@@ -2046,3 +2225,32 @@ func extractChartMeta(chartData []byte) (*hchart.Metadata, error) {
 	}
 	return ch.Metadata, nil
 }
+
+func loadTestChartToOCI(chartData []byte, chartPath string, server *registryClientTestServer) (*hchart.Metadata, error) {
+	// Login to the registry
+	err := server.registryClient.Login(server.registryHost,
+		helmreg.LoginOptBasicAuth(testRegistryUsername, testRegistryPassword),
+		helmreg.LoginOptInsecure(true))
+	if err != nil {
+		return nil, err
+	}
+
+	// Load a test chart
+	chartData, err = ioutil.ReadFile(chartPath)
+	if err != nil {
+		return nil, err
+	}
+	metadata, err := extractChartMeta(chartData)
+	if err != nil {
+		return nil, err
+	}
+
+	// Upload the test chart
+	ref := fmt.Sprintf("%s/testrepo/%s:%s", server.registryHost, metadata.Name, metadata.Version)
+	_, err = server.registryClient.Push(chartData, ref)
+	if err != nil {
+		return nil, err
+	}
+
+	return metadata, nil
+}
diff --git a/controllers/helmrepository_controller_oci.go b/controllers/helmrepository_controller_oci.go
index cb2df389c..02ec39b49 100644
--- a/controllers/helmrepository_controller_oci.go
+++ b/controllers/helmrepository_controller_oci.go
@@ -22,7 +22,6 @@ import (
 	"fmt"
 	"net/url"
 	"os"
-	"strings"
 	"time"
 
 	helmgetter "helm.sh/helm/v3/pkg/getter"
@@ -42,12 +41,10 @@ import (
 
 	"github.com/fluxcd/pkg/apis/meta"
 	"github.com/fluxcd/pkg/oci"
-	"github.com/fluxcd/pkg/oci/auth/login"
 	"github.com/fluxcd/pkg/runtime/conditions"
 	helper "github.com/fluxcd/pkg/runtime/controller"
 	"github.com/fluxcd/pkg/runtime/patch"
 	"github.com/fluxcd/pkg/runtime/predicates"
-	"github.com/google/go-containerregistry/pkg/name"
 
 	"github.com/fluxcd/source-controller/api/v1beta2"
 	sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
@@ -294,10 +291,8 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta
 		if loginOpt != nil {
 			loginOpts = append(loginOpts, loginOpt)
 		}
-	}
-
-	if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
-		auth, authErr := oidcAuth(ctxTimeout, obj)
+	} else if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
+		auth, authErr := oidcAuthFromAdapter(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
 		if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
 			e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
 			conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
@@ -380,41 +375,12 @@ func (r *HelmRepositoryOCIReconciler) eventLogf(ctx context.Context, obj runtime
 	r.Eventf(obj, eventType, reason, msg)
 }
 
-// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
-func oidcAuth(ctx context.Context, obj *sourcev1.HelmRepository) (helmreg.LoginOption, error) {
-	url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix)
-	ref, err := name.ParseReference(url)
-	if err != nil {
-		return nil, fmt.Errorf("failed to parse URL '%s': %w", obj.Spec.URL, err)
-	}
-
-	loginOpt, err := loginWithManager(ctx, obj.Spec.Provider, url, ref)
-	if err != nil {
-		return nil, fmt.Errorf("failed to login to registry '%s': %w", obj.Spec.URL, err)
-	}
-
-	return loginOpt, nil
-}
-
-func loginWithManager(ctx context.Context, provider, url string, ref name.Reference) (helmreg.LoginOption, error) {
-	opts := login.ProviderOptions{}
-	switch provider {
-	case sourcev1.AmazonOCIProvider:
-		opts.AwsAutoLogin = true
-	case sourcev1.AzureOCIProvider:
-		opts.AzureAutoLogin = true
-	case sourcev1.GoogleOCIProvider:
-		opts.GcpAutoLogin = true
-	}
-
-	auth, err := login.NewManager().Login(ctx, url, ref, opts)
+// oidcAuthFromAdapter generates the OIDC credential authenticator based on the specified cloud provider.
+func oidcAuthFromAdapter(ctx context.Context, url, provider string) (helmreg.LoginOption, error) {
+	auth, err := oidcAuth(ctx, url, provider)
 	if err != nil {
 		return nil, err
 	}
 
-	if auth == nil {
-		return nil, nil
-	}
-
 	return registry.OIDCAdaptHelper(auth)
 }
diff --git a/controllers/helmrepository_controller_oci_test.go b/controllers/helmrepository_controller_oci_test.go
index ec75a67ef..c5e36c297 100644
--- a/controllers/helmrepository_controller_oci_test.go
+++ b/controllers/helmrepository_controller_oci_test.go
@@ -26,12 +26,16 @@ import (
 	"github.com/fluxcd/pkg/runtime/conditions"
 	"github.com/fluxcd/pkg/runtime/patch"
 	sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
+	"github.com/fluxcd/source-controller/internal/helm/registry"
 	. "github.com/onsi/gomega"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/tools/record"
 	kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status"
+	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
 
 func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
@@ -162,3 +166,148 @@ func TestHelmRepositoryOCIReconciler_Reconcile(t *testing.T) {
 		})
 	}
 }
+
+func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
+	type secretOptions struct {
+		username string
+		password string
+	}
+
+	tests := []struct {
+		name             string
+		url              string
+		registryOpts     registryOptions
+		secretOpts       secretOptions
+		provider         string
+		providerImg      string
+		want             ctrl.Result
+		wantErr          bool
+		assertConditions []metav1.Condition
+	}{
+		{
+			name: "HTTP without basic auth",
+			want: ctrl.Result{RequeueAfter: interval},
+			assertConditions: []metav1.Condition{
+				*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Helm repository is ready"),
+			},
+		},
+		{
+			name: "HTTP with basic auth secret",
+			want: ctrl.Result{RequeueAfter: interval},
+			registryOpts: registryOptions{
+				withBasicAuth: true,
+			},
+			secretOpts: secretOptions{
+				username: testRegistryUsername,
+				password: testRegistryPassword,
+			},
+			assertConditions: []metav1.Condition{
+				*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Helm repository is ready"),
+			},
+		},
+		{
+			name:    "HTTP registry - basic auth with invalid secret",
+			want:    ctrl.Result{},
+			wantErr: true,
+			registryOpts: registryOptions{
+				withBasicAuth: true,
+			},
+			secretOpts: secretOptions{
+				username: "wrong-pass",
+				password: "wrong-pass",
+			},
+			assertConditions: []metav1.Condition{
+				*conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "failed to login to registry"),
+			},
+		},
+		{
+			name:        "with contextual login provider",
+			wantErr:     true,
+			provider:    "aws",
+			providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test",
+			assertConditions: []metav1.Condition{
+				*conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "failed to get credential from"),
+			},
+		},
+		{
+			name: "with contextual login provider and secretRef",
+			want: ctrl.Result{RequeueAfter: interval},
+			registryOpts: registryOptions{
+				withBasicAuth: true,
+			},
+			secretOpts: secretOptions{
+				username: testRegistryUsername,
+				password: testRegistryPassword,
+			},
+			provider: "azure",
+			assertConditions: []metav1.Condition{
+				*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "Helm repository is ready"),
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			g := NewWithT(t)
+
+			builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme())
+			workspaceDir := t.TempDir()
+			server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts)
+			g.Expect(err).NotTo(HaveOccurred())
+
+			obj := &sourcev1.HelmRepository{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "auth-strategy-",
+				},
+				Spec: sourcev1.HelmRepositorySpec{
+					Interval: metav1.Duration{Duration: interval},
+					Timeout:  &metav1.Duration{Duration: timeout},
+					Type:     sourcev1.HelmRepositoryTypeOCI,
+					Provider: sourcev1.GenericOCIProvider,
+					URL:      fmt.Sprintf("oci://%s", server.registryHost),
+				},
+			}
+
+			if tt.provider != "" {
+				obj.Spec.Provider = tt.provider
+			}
+			// If a provider specific image is provided, overwrite existing URL
+			// set earlier. It'll fail but it's necessary to set them because
+			// the login check expects the URLs to be of certain pattern.
+			if tt.providerImg != "" {
+				obj.Spec.URL = tt.providerImg
+			}
+
+			if tt.secretOpts.username != "" && tt.secretOpts.password != "" {
+				secret := &corev1.Secret{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "auth-secretref",
+					},
+					Type: corev1.SecretTypeDockerConfigJson,
+					Data: map[string][]byte{
+						".dockerconfigjson": []byte(fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`,
+							server.registryHost, tt.secretOpts.username, tt.secretOpts.password)),
+					},
+				}
+
+				builder.WithObjects(secret)
+
+				obj.Spec.SecretRef = &meta.LocalObjectReference{
+					Name: secret.Name,
+				}
+			}
+
+			r := &HelmRepositoryOCIReconciler{
+				Client:                  builder.Build(),
+				EventRecorder:           record.NewFakeRecorder(32),
+				Getters:                 testGetters,
+				RegistryClientGenerator: registry.ClientGenerator,
+			}
+
+			got, err := r.reconcile(ctx, obj)
+			g.Expect(err != nil).To(Equal(tt.wantErr))
+			g.Expect(got).To(Equal(tt.want))
+			g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
+		})
+	}
+}
diff --git a/controllers/ocirepository_controller.go b/controllers/ocirepository_controller.go
index 1e8744b02..b05c5e8b3 100644
--- a/controllers/ocirepository_controller.go
+++ b/controllers/ocirepository_controller.go
@@ -308,8 +308,8 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
 	}
 	options = append(options, crane.WithAuthFromKeychain(keychain))
 
-	if obj.Spec.Provider != sourcev1.GenericOCIProvider {
-		auth, authErr := r.oidcAuth(ctxTimeout, obj)
+	if _, ok := keychain.(util.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok {
+		auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
 		if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
 			e := serror.NewGeneric(
 				fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
@@ -589,9 +589,9 @@ func (r *OCIRepositoryReconciler) keychain(ctx context.Context, obj *sourcev1.OC
 		}
 	}
 
-	// if no pullsecrets available return DefaultKeyChain
+	// if no pullsecrets available return an AnonymousKeychain
 	if len(pullSecretNames) == 0 {
-		return authn.DefaultKeychain, nil
+		return util.Anonymous{}, nil
 	}
 
 	// lookup image pull secrets
@@ -655,15 +655,15 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O
 }
 
 // oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
-func (r *OCIRepositoryReconciler) oidcAuth(ctx context.Context, obj *sourcev1.OCIRepository) (authn.Authenticator, error) {
-	url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix)
-	ref, err := name.ParseReference(url)
+func oidcAuth(ctx context.Context, url, provider string) (authn.Authenticator, error) {
+	u := strings.TrimPrefix(url, sourcev1.OCIRepositoryPrefix)
+	ref, err := name.ParseReference(u)
 	if err != nil {
-		return nil, fmt.Errorf("failed to parse URL '%s': %w", obj.Spec.URL, err)
+		return nil, fmt.Errorf("failed to parse URL '%s': %w", u, err)
 	}
 
 	opts := login.ProviderOptions{}
-	switch obj.Spec.Provider {
+	switch provider {
 	case sourcev1.AmazonOCIProvider:
 		opts.AwsAutoLogin = true
 	case sourcev1.AzureOCIProvider:
@@ -672,7 +672,7 @@ func (r *OCIRepositoryReconciler) oidcAuth(ctx context.Context, obj *sourcev1.OC
 		opts.GcpAutoLogin = true
 	}
 
-	return login.NewManager().Login(ctx, url, ref, opts)
+	return login.NewManager().Login(ctx, u, ref, opts)
 }
 
 // craneOptions sets the auth headers, timeout and user agent
diff --git a/controllers/ocirepository_controller_test.go b/controllers/ocirepository_controller_test.go
index b08527bfd..f6fe50118 100644
--- a/controllers/ocirepository_controller_test.go
+++ b/controllers/ocirepository_controller_test.go
@@ -369,6 +369,8 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) {
 		craneOpts        []crane.Option
 		secretOpts       secretOptions
 		tlsCertSecret    *corev1.Secret
+		provider         string
+		providerImg      string
 		want             sreconcile.Result
 		wantErr          bool
 		assertConditions []metav1.Condition
@@ -548,6 +550,36 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) {
 				*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "failed to pull artifact from "),
 			},
 		},
+		{
+			name:        "with contextual login provider",
+			wantErr:     true,
+			provider:    "aws",
+			providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test",
+			assertConditions: []metav1.Condition{
+				*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "failed to get credential from"),
+			},
+		},
+		{
+			name: "with contextual login provider and secretRef",
+			want: sreconcile.ResultSuccess,
+			registryOpts: registryOptions{
+				withBasicAuth: true,
+			},
+			craneOpts: []crane.Option{crane.WithAuth(&authn.Basic{
+				Username: testRegistryUsername,
+				Password: testRegistryPassword,
+			})},
+			secretOpts: secretOptions{
+				username:      testRegistryUsername,
+				password:      testRegistryPassword,
+				includeSecret: true,
+			},
+			provider: "azure",
+			assertConditions: []metav1.Condition{
+				*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
+				*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
+			},
+		},
 	}
 
 	for _, tt := range tests {
@@ -578,6 +610,16 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) {
 				Tag: img.tag,
 			}
 
+			if tt.provider != "" {
+				obj.Spec.Provider = tt.provider
+			}
+			// If a provider specific image is provided, overwrite existing URL
+			// set earlier. It'll fail but it's necessary to set them because
+			// the login check expects the URLs to be of certain pattern.
+			if tt.providerImg != "" {
+				obj.Spec.URL = tt.providerImg
+			}
+
 			if tt.secretOpts.username != "" && tt.secretOpts.password != "" {
 				secret := &corev1.Secret{
 					ObjectMeta: metav1.ObjectMeta{
diff --git a/controllers/suite_test.go b/controllers/suite_test.go
index b2956b58c..8654f06f4 100644
--- a/controllers/suite_test.go
+++ b/controllers/suite_test.go
@@ -36,10 +36,12 @@ import (
 	"k8s.io/client-go/tools/record"
 	ctrl "sigs.k8s.io/controller-runtime"
 
+	dcontext "github.com/distribution/distribution/v3/context"
 	"github.com/fluxcd/pkg/runtime/controller"
 	"github.com/fluxcd/pkg/runtime/testenv"
 	"github.com/fluxcd/pkg/testserver"
 	"github.com/phayes/freeport"
+	"github.com/sirupsen/logrus"
 
 	"github.com/distribution/distribution/v3/configuration"
 	dockerRegistry "github.com/distribution/distribution/v3/registry"
@@ -153,8 +155,6 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry
 	server.registryHost = fmt.Sprintf("localhost:%d", port)
 	config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
 	config.HTTP.DrainTimeout = time.Duration(10) * time.Second
-	config.Log.AccessLog.Disabled = true
-	config.Log.Level = "error"
 	config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
 
 	if opts.withBasicAuth {
@@ -184,6 +184,13 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry
 		config.HTTP.TLS.Key = "testdata/certs/server-key.pem"
 	}
 
+	// setup logger options
+	config.Log.AccessLog.Disabled = true
+	config.Log.Level = "error"
+	logger := logrus.New()
+	logger.SetOutput(io.Discard)
+	dcontext.SetDefaultLogger(logrus.NewEntry(logger))
+
 	dockerRegistry, err := dockerRegistry.NewRegistry(ctx, config)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create docker registry: %w", err)
diff --git a/internal/util/auth.go b/internal/util/auth.go
new file mode 100644
index 000000000..8b944cc31
--- /dev/null
+++ b/internal/util/auth.go
@@ -0,0 +1,30 @@
+/*
+Copyright 2022 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package util
+
+import "github.com/google/go-containerregistry/pkg/authn"
+
+// Anonymous is an authn.AuthConfig that always returns an anonymous
+// authenticator. It is useful for registries that do not require authentication
+// or when the credentials are not known.
+// It implements authn.Keychain `Resolve` method and can be used as a keychain.
+type Anonymous authn.AuthConfig
+
+// Resolve implements authn.Keychain.
+func (a Anonymous) Resolve(_ authn.Resource) (authn.Authenticator, error) {
+	return authn.Anonymous, nil
+}