diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index 54a3c856..4c510b07 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -65,9 +65,11 @@ body:
Windows (PowerShell):
$env:ARTIFACTS_CREDENTIALPROVIDER_LOG_PATH = "$PWD\\artifacts-credprovider.log"
+ $env:ARTIFACTS_CREDENTIALPROVIDER_REDACT_ENABLED = "true" # Optional: redact sensitive info
Linux/macOS (bash/zsh):
export ARTIFACTS_CREDENTIALPROVIDER_LOG_PATH="$PWD/artifacts-credprovider.log"
+ export ARTIFACTS_CREDENTIALPROVIDER_REDACT_ENABLED="true" # Optional: redact sensitive info
2) Run your failing command with detailed verbosity:
@@ -83,7 +85,8 @@ body:
3) Provide the log output below with any urls or secrets redacted.
Note:
- Legacy log path variable is NUGET_CREDENTIALPROVIDER_LOG_PATH.
- - Remove/redact urls before posting logs.
+ - Setting ARTIFACTS_CREDENTIALPROVIDER_REDACT_ENABLED=true provides best-effort automatic redaction.
+ - Redaction is not guaranteed to catch everything. ALWAYS review logs before posting to ensure no sensitive data is exposed.
validations:
required: false
diff --git a/CredentialProvider.Microsoft.Tests/Logging/LogEveryMessageFileLoggerTests.cs b/CredentialProvider.Microsoft.Tests/Logging/LogEveryMessageFileLoggerTests.cs
new file mode 100644
index 00000000..a7db9188
--- /dev/null
+++ b/CredentialProvider.Microsoft.Tests/Logging/LogEveryMessageFileLoggerTests.cs
@@ -0,0 +1,80 @@
+// Copyright (c) Microsoft. All rights reserved.
+//
+// Licensed under the MIT license.
+
+using System;
+using System.IO;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NuGet.Common;
+using NuGetCredentialProvider.Logging;
+
+namespace CredentialProvider.Microsoft.Tests.Logging
+{
+ [TestClass]
+ public class LogEveryMessageFileLoggerTests
+ {
+ private string tempLogFile;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ tempLogFile = Path.GetTempFileName();
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ if (File.Exists(tempLogFile))
+ {
+ try
+ {
+ File.Delete(tempLogFile);
+ }
+ catch
+ {
+ // Ignore cleanup errors
+ }
+ }
+ }
+
+ [TestMethod]
+ public void Log_WritesMessageToFile()
+ {
+ // Arrange
+ var logger = new LogEveryMessageFileLogger(tempLogFile);
+ var message = "Test message";
+
+ // Act
+ logger.Log(LogLevel.Information, allowOnConsole: false, message);
+
+ // Assert
+ var logContent = File.ReadAllText(tempLogFile);
+ Assert.IsTrue(logContent.Contains("Test message"), "Message should be written to file");
+ }
+
+ [TestMethod]
+ public void Log_PreservesNonSensitiveContent()
+ {
+ // Arrange
+ var logger = new LogEveryMessageFileLogger(tempLogFile);
+ var message = "Successfully authenticated to service";
+
+ // Act
+ logger.Log(LogLevel.Information, allowOnConsole: false, message);
+
+ // Assert
+ var logContent = File.ReadAllText(tempLogFile);
+ Assert.IsTrue(logContent.Contains("Successfully authenticated to service"), "Non-sensitive content should be preserved");
+ }
+
+ [TestMethod]
+ public void Log_HandlesNullMessage()
+ {
+ // Arrange
+ var logger = new LogEveryMessageFileLogger(tempLogFile);
+
+ // Act & Assert - should not throw
+ logger.Log(LogLevel.Verbose, allowOnConsole: false, null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CredentialProvider.Microsoft.Tests/Util/RedactionUtilTests.cs b/CredentialProvider.Microsoft.Tests/Util/RedactionUtilTests.cs
new file mode 100644
index 00000000..9108769e
--- /dev/null
+++ b/CredentialProvider.Microsoft.Tests/Util/RedactionUtilTests.cs
@@ -0,0 +1,220 @@
+// Copyright (c) Microsoft. All rights reserved.
+//
+// Licensed under the MIT license.
+
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using NuGetCredentialProvider.Util;
+
+namespace CredentialProvider.Microsoft.Tests.Util
+{
+ [TestClass]
+ public class RedactionUtilTests
+ {
+ [TestInitialize]
+ public void Setup()
+ {
+ // Reset redaction flag before each test
+ RedactionUtil.ShouldRedact = false;
+ }
+
+ [TestMethod]
+ public void RedactFeedUrl_WhenRedactionDisabled_ReturnsOriginalUrl()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = false;
+ var url = "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json";
+
+ // Act
+ var result = RedactionUtil.RedactFeedUrl(url);
+
+ // Assert
+ Assert.AreEqual(url, result);
+ }
+
+ [TestMethod]
+ public void RedactFeedUrl_WhenRedactionEnabled_RedactsAzureDevOpsUrl()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = true;
+ var url = "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json";
+
+ // Act
+ var result = RedactionUtil.RedactFeedUrl(url);
+
+ // Assert
+ Assert.IsTrue(result.Contains("[REDACTED_FEED_URL]"));
+ Assert.IsFalse(result.Contains("myorg"));
+ Assert.IsFalse(result.Contains("myfeed"));
+ }
+
+ [TestMethod]
+ public void RedactFeedUrl_WhenRedactionEnabled_RedactsVisualStudioUrl()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = true;
+ var url = "https://myorg.pkgs.visualstudio.com/_packaging/myfeed/nuget/v3/index.json";
+
+ // Act
+ var result = RedactionUtil.RedactFeedUrl(url);
+
+ // Assert
+ Assert.IsTrue(result.Contains("[REDACTED_FEED_URL]"));
+ Assert.IsFalse(result.Contains("myorg"));
+ Assert.IsFalse(result.Contains("myfeed"));
+ }
+
+ [TestMethod]
+ public void RedactFeedUrl_WhenRedactionEnabled_RedactsDevAzureComUrl()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = true;
+ var url = "https://dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json";
+
+ // Act
+ var result = RedactionUtil.RedactFeedUrl(url);
+
+ // Assert
+ Assert.IsTrue(result.Contains("[REDACTED_FEED_URL]"));
+ Assert.IsFalse(result.Contains("myorg"));
+ Assert.IsFalse(result.Contains("myfeed"));
+ }
+
+ [TestMethod]
+ public void RedactFeedUrl_WhenRedactionEnabled_RedactsOnPremUrl()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = true;
+ var url = "https://myserver.company.com/tfs/DefaultCollection/_packaging/myfeed/nuget/v3/index.json";
+
+ // Act
+ var result = RedactionUtil.RedactFeedUrl(url);
+
+ // Assert
+ Assert.IsTrue(result.Contains("[REDACTED_FEED_URL]"));
+ Assert.IsFalse(result.Contains("myserver"));
+ Assert.IsFalse(result.Contains("DefaultCollection"));
+ Assert.IsFalse(result.Contains("myfeed"));
+ }
+
+ [TestMethod]
+ public void RedactFeedUrl_WithUri_WhenRedactionEnabled_RedactsUrl()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = true;
+ var uri = new Uri("https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json");
+
+ // Act
+ var result = RedactionUtil.RedactFeedUrl(uri);
+
+ // Assert
+ Assert.IsTrue(result.Contains("[REDACTED_FEED_URL]"));
+ Assert.IsFalse(result.Contains("myorg"));
+ }
+
+ [TestMethod]
+ public void RedactFeedUrl_WithNullUri_ReturnsNull()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = true;
+ Uri uri = null;
+
+ // Act
+ var result = RedactionUtil.RedactFeedUrl(uri);
+
+ // Assert
+ Assert.IsNull(result);
+ }
+
+ [TestMethod]
+ public void ShouldRedact_CanBeSetAndRetrieved()
+ {
+ // Arrange & Act
+ RedactionUtil.ShouldRedact = true;
+
+ // Assert
+ Assert.IsTrue(RedactionUtil.ShouldRedact);
+
+ // Arrange & Act
+ RedactionUtil.ShouldRedact = false;
+
+ // Assert
+ Assert.IsFalse(RedactionUtil.ShouldRedact);
+ }
+
+ [TestMethod]
+ public void RedactFeedUrl_WhenRedactionEnabled_RedactsAllUrls()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = true;
+
+ // Act & Assert - Various feed URLs should all be redacted
+ var nugetOrg = RedactionUtil.RedactFeedUrl("https://api.nuget.org/v3/index.json");
+ Assert.AreEqual("https://[REDACTED_FEED_URL]", nugetOrg);
+
+ var github = RedactionUtil.RedactFeedUrl("https://nuget.pkg.github.com/myorg/index.json");
+ Assert.AreEqual("https://[REDACTED_FEED_URL]", github);
+
+ var myget = RedactionUtil.RedactFeedUrl("https://www.myget.org/F/myfeed/api/v3/index.json");
+ Assert.AreEqual("https://[REDACTED_FEED_URL]", myget);
+
+ var privateServer = RedactionUtil.RedactFeedUrl("https://packages.internal.company.com/nuget/v3/index.json");
+ Assert.AreEqual("https://[REDACTED_FEED_URL]", privateServer);
+ }
+
+ [TestMethod]
+ public void RedactPassword_WhenRedactionDisabled_ReturnsOriginalPassword()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = false;
+ var password = "abc123secretToken";
+
+ // Act
+ var result = RedactionUtil.RedactPassword(password);
+
+ // Assert
+ Assert.AreEqual(password, result);
+ }
+
+ [TestMethod]
+ public void RedactPassword_WhenRedactionEnabled_RedactsPassword()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = true;
+ var password = "abc123secretToken";
+
+ // Act
+ var result = RedactionUtil.RedactPassword(password);
+
+ // Assert
+ Assert.AreEqual("[REDACTED]", result);
+ Assert.AreNotEqual(password, result);
+ }
+
+ [TestMethod]
+ public void RedactPassword_WithNull_ReturnsNull()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = true;
+
+ // Act
+ var result = RedactionUtil.RedactPassword(null);
+
+ // Assert
+ Assert.IsNull(result);
+ }
+
+ [TestMethod]
+ public void RedactPassword_WithEmptyString_ReturnsEmptyString()
+ {
+ // Arrange
+ RedactionUtil.ShouldRedact = true;
+
+ // Act
+ var result = RedactionUtil.RedactPassword("");
+
+ // Assert
+ Assert.AreEqual("", result);
+ }
+ }
+}
diff --git a/CredentialProvider.Microsoft/CredentialProvider.Microsoft.csproj b/CredentialProvider.Microsoft/CredentialProvider.Microsoft.csproj
index 34195cd5..aa2be8bd 100644
--- a/CredentialProvider.Microsoft/CredentialProvider.Microsoft.csproj
+++ b/CredentialProvider.Microsoft/CredentialProvider.Microsoft.csproj
@@ -38,6 +38,12 @@
$(DefineConstants);TRACE
+
+
+ <_Parameter1>CredentialProvider.Microsoft.Tests
+
+
+
diff --git a/CredentialProvider.Microsoft/CredentialProviderArgs.cs b/CredentialProvider.Microsoft/CredentialProviderArgs.cs
index efbdf57c..02404d06 100644
--- a/CredentialProvider.Microsoft/CredentialProviderArgs.cs
+++ b/CredentialProvider.Microsoft/CredentialProviderArgs.cs
@@ -31,7 +31,7 @@ internal class CredentialProviderArgs
[ArgDescription("Display this amount of detail in the output")]
public LogLevel Verbosity { get; set; }
- [ArgDescription("Prevents writing the password to standard output (for troubleshooting purposes)")]
+ [ArgDescription("Redacts sensitive information from output: passwords/tokens and feed URLs. This is a best-effort feature. Always review logs before sharing.")]
public bool RedactPassword { get; set; }
[ArgShortcut("?")]
diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/IAuthUtil.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/IAuthUtil.cs
index 01e4986f..4797f6b1 100644
--- a/CredentialProvider.Microsoft/CredentialProviders/Vsts/IAuthUtil.cs
+++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/IAuthUtil.cs
@@ -102,7 +102,7 @@ public async Task GetAuthorizationEndpoint(Uri uri, CancellationToken cance
logger.Warning(e.StackTrace);
}
- logger.Warning(string.Format(Resources.SPSAuthEndpointNotFound, uri.ToString()));
+ logger.Warning(string.Format(Resources.SPSAuthEndpointNotFound, RedactionUtil.RedactFeedUrl(uri.ToString())));
return null;
}
@@ -117,7 +117,7 @@ protected virtual async Task GetResponseHeadersAsync(Uri ur
using (var request = new HttpRequestMessage(HttpMethod.Get, uri))
{
- logger.Verbose($"GET {uri}");
+ logger.Verbose($"GET {RedactionUtil.RedactFeedUrl(uri)}");
using (var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
{
cache[uri] = response.Headers;
diff --git a/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsCredentialProvider.cs b/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsCredentialProvider.cs
index 6db5178e..3ef1102e 100644
--- a/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsCredentialProvider.cs
+++ b/CredentialProvider.Microsoft/CredentialProviders/Vsts/VstsCredentialProvider.cs
@@ -116,7 +116,7 @@ public override async Task HandleRequestAs
InteractiveTimeout = TimeSpan.FromSeconds(EnvUtil.GetDeviceFlowTimeoutFromEnvironmentInSeconds(Logger)),
DeviceCodeResultCallback = (DeviceCodeResult deviceCodeResult) =>
{
- Logger.Minimal(string.Format(Resources.DeviceFlowRequestedResource, request.Uri.ToString()));
+ Logger.Minimal(string.Format(Resources.DeviceFlowRequestedResource, RedactionUtil.RedactFeedUrl(request.Uri.ToString())));
Logger.Minimal(string.Format(Resources.DeviceFlowMessage, deviceCodeResult.VerificationUrl, deviceCodeResult.UserCode));
return Task.CompletedTask;
@@ -172,7 +172,7 @@ public override async Task HandleRequestAs
if (!string.IsNullOrWhiteSpace(sessionToken))
{
- Verbose(string.Format(Resources.VSTSSessionTokenCreated, request.Uri.AbsoluteUri));
+ Verbose(string.Format(Resources.VSTSSessionTokenCreated, RedactionUtil.RedactFeedUrl(request.Uri)));
return new GetAuthenticationCredentialsResponse(
Username,
sessionToken,
@@ -183,11 +183,11 @@ public override async Task HandleRequestAs
}
catch (Exception e)
{
- Verbose(string.Format(Resources.VSTSCreateSessionException, request.Uri.AbsoluteUri, e.Message, e.StackTrace));
+ Verbose(string.Format(Resources.VSTSCreateSessionException, RedactionUtil.RedactFeedUrl(request.Uri), e.Message, e.StackTrace));
}
}
- Verbose(string.Format(Resources.VSTSCredentialsNotFound, request.Uri.AbsoluteUri));
+ Verbose(string.Format(Resources.VSTSCredentialsNotFound, RedactionUtil.RedactFeedUrl(request.Uri)));
return null;
}
}
diff --git a/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTask/VstsBuildTaskCredentialProvider.cs b/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTask/VstsBuildTaskCredentialProvider.cs
index cf1b5a62..0e54a990 100644
--- a/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTask/VstsBuildTaskCredentialProvider.cs
+++ b/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTask/VstsBuildTaskCredentialProvider.cs
@@ -60,7 +60,7 @@ public override Task HandleRequestAsync(Ge
string uriString = request.Uri.AbsoluteUri;
string matchedPrefix = uriPrefixes.FirstOrDefault(prefix => uriString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
- Verbose(string.Format(Resources.BuildTaskMatchedPrefix, matchedPrefix != null ? matchedPrefix : Resources.BuildTaskNoMatchingPrefixes));
+ Verbose(string.Format(Resources.BuildTaskMatchedPrefix, matchedPrefix != null ? RedactionUtil.RedactFeedUrl(matchedPrefix) : Resources.BuildTaskNoMatchingPrefixes));
if (matchedPrefix == null)
{
@@ -72,7 +72,7 @@ public override Task HandleRequestAsync(Ge
MessageResponseCode.Error);
}
- Verbose(string.Format(Resources.BuildTaskEndpointMatchingUrlFound, uriString));
+ Verbose(string.Format(Resources.BuildTaskEndpointMatchingUrlFound, RedactionUtil.RedactFeedUrl(uriString)));
return this.GetResponse(
Username,
accessToken,
diff --git a/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTaskServiceEndpoint/VstsBuildTaskServiceEndpointCredentialProvider.cs b/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTaskServiceEndpoint/VstsBuildTaskServiceEndpointCredentialProvider.cs
index a770252f..d8fce1ae 100644
--- a/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTaskServiceEndpoint/VstsBuildTaskServiceEndpointCredentialProvider.cs
+++ b/CredentialProvider.Microsoft/CredentialProviders/VstsBuildTaskServiceEndpoint/VstsBuildTaskServiceEndpointCredentialProvider.cs
@@ -69,7 +69,7 @@ public override async Task HandleRequestAs
bool externalEndpointFound = ExternalCredentials.TryGetValue(uriString, out ExternalEndpointCredentials matchingExternalEndpoint);
if (externalEndpointFound && !string.IsNullOrWhiteSpace(matchingExternalEndpoint.Password))
{
- Verbose(string.Format(Resources.BuildTaskEndpointMatchingUrlFound, uriString));
+ Verbose(string.Format(Resources.BuildTaskEndpointMatchingUrlFound, RedactionUtil.RedactFeedUrl(uriString)));
return GetResponse(
matchingExternalEndpoint.Username,
matchingExternalEndpoint.Password,
@@ -142,11 +142,11 @@ public override async Task HandleRequestAs
}
}
- Verbose(string.Format(Resources.BuildTaskEndpointNoMatchingUrl, uriString));
+ Verbose(string.Format(Resources.BuildTaskEndpointNoMatchingUrl, RedactionUtil.RedactFeedUrl(uriString)));
return GetResponse(
null,
null,
- string.Format(Resources.BuildTaskFailedToAuthenticate, uriString),
+ string.Format(Resources.BuildTaskFailedToAuthenticate, RedactionUtil.RedactFeedUrl(uriString)),
MessageResponseCode.Error);
}
diff --git a/CredentialProvider.Microsoft/Program.cs b/CredentialProvider.Microsoft/Program.cs
index 67b7053d..ab2d5c9c 100644
--- a/CredentialProvider.Microsoft/Program.cs
+++ b/CredentialProvider.Microsoft/Program.cs
@@ -59,6 +59,9 @@ public static async Task BackgroundWork(string[] args)
CancellationTokenSource tokenSource = new CancellationTokenSource();
var parsedArgs = await Args.ParseAsync(args);
+ // Set redaction flag: command-line arg takes precedence, then env var
+ RedactionUtil.ShouldRedact = parsedArgs.RedactPassword || EnvUtil.GetRedactionEnabledFromEnvironment();
+
var multiLogger = new MultiLogger();
var fileLogger = GetFileLogger();
if (fileLogger != null)
@@ -142,7 +145,8 @@ public static async Task BackgroundWork(string[] args)
EnvUtil.MsalFileCacheEnvVar,
EnvUtil.LegacyMsalFileCacheEnvVar,
EnvUtil.MsalFileCacheLocationEnvVar,
- EnvUtil.LegacyMsalFileCacheLocationEnvVar
+ EnvUtil.LegacyMsalFileCacheLocationEnvVar,
+ EnvUtil.RedactEnabledEnvVar
));
return 0;
}
@@ -208,7 +212,7 @@ public static async Task BackgroundWork(string[] args)
}
string resultUsername = response?.Username;
- string resultPassword = parsedArgs.RedactPassword ? Resources.Redacted : response?.Password;
+ string resultPassword = RedactionUtil.RedactPassword(response?.Password);
if (parsedArgs.OutputFormat == OutputFormat.Json)
{
// Manually write the JSON output, since we don't use ConsoleLogger in JSON mode (see above)
diff --git a/CredentialProvider.Microsoft/RequestHandlers/GetAuthenticationCredentialsRequestHandler.cs b/CredentialProvider.Microsoft/RequestHandlers/GetAuthenticationCredentialsRequestHandler.cs
index f47888b0..8e0da814 100644
--- a/CredentialProvider.Microsoft/RequestHandlers/GetAuthenticationCredentialsRequestHandler.cs
+++ b/CredentialProvider.Microsoft/RequestHandlers/GetAuthenticationCredentialsRequestHandler.cs
@@ -43,7 +43,7 @@ public GetAuthenticationCredentialsRequestHandler(ILogger logger, IReadOnlyColle
public override async Task HandleRequestAsync(GetAuthenticationCredentialsRequest request)
{
- Logger.Verbose(string.Format(Resources.HandlingAuthRequest, request.Uri.AbsoluteUri, request.IsRetry, request.IsNonInteractive, request.CanShowDialog));
+ Logger.Verbose(string.Format(Resources.HandlingAuthRequest, RedactionUtil.RedactFeedUrl(request.Uri), request.IsRetry, request.IsNonInteractive, request.CanShowDialog));
if (request?.Uri == null)
{
@@ -56,16 +56,16 @@ public override async Task HandleRequestAs
responseCode: MessageResponseCode.Error);
}
- Logger.Verbose(string.Format(Resources.Uri, request.Uri.AbsoluteUri));
+ Logger.Verbose(string.Format(Resources.Uri, RedactionUtil.RedactFeedUrl(request.Uri)));
foreach (ICredentialProvider credentialProvider in credentialProviders)
{
if (await credentialProvider.CanProvideCredentialsAsync(request.Uri) == false)
{
- Logger.Verbose(string.Format(Resources.SkippingCredentialProvider, credentialProvider, request.Uri.AbsoluteUri));
+ Logger.Verbose(string.Format(Resources.SkippingCredentialProvider, credentialProvider, RedactionUtil.RedactFeedUrl(request.Uri)));
continue;
}
- Logger.Verbose(string.Format(Resources.UsingCredentialProvider, credentialProvider, request.Uri.AbsoluteUri));
+ Logger.Verbose(string.Format(Resources.UsingCredentialProvider, credentialProvider, RedactionUtil.RedactFeedUrl(request.Uri)));
if (credentialProvider.IsCachable && TryCache(request, out string cachedToken))
{
@@ -84,7 +84,7 @@ public override async Task HandleRequestAs
{
if (cache != null && credentialProvider.IsCachable)
{
- Logger.Verbose(string.Format(Resources.CachingSessionToken, request.Uri.AbsoluteUri));
+ Logger.Verbose(string.Format(Resources.CachingSessionToken, RedactionUtil.RedactFeedUrl(request.Uri)));
cache[request.Uri] = response.Password;
}
@@ -149,18 +149,18 @@ private bool TryCache(GetAuthenticationCredentialsRequest request, out string ca
Logger.Verbose(string.Format(Resources.IsRetry, request.IsRetry));
if (request.IsRetry)
{
- Logger.Verbose(string.Format(Resources.InvalidatingCachedSessionToken, request.Uri.AbsoluteUri));
+ Logger.Verbose(string.Format(Resources.InvalidatingCachedSessionToken, RedactionUtil.RedactFeedUrl(request.Uri)));
cache?.Remove(request.Uri);
return false;
}
else if (cache.TryGetValue(request.Uri, out string password))
{
- Logger.Verbose(string.Format(Resources.FoundCachedSessionToken, request.Uri.AbsoluteUri));
+ Logger.Verbose(string.Format(Resources.FoundCachedSessionToken, RedactionUtil.RedactFeedUrl(request.Uri)));
cachedToken = password;
return true;
}
- Logger.Verbose(string.Format(Resources.CachedSessionTokenNotFound, request.Uri.AbsoluteUri));
+ Logger.Verbose(string.Format(Resources.CachedSessionTokenNotFound, RedactionUtil.RedactFeedUrl(request.Uri)));
return false;
}
}
diff --git a/CredentialProvider.Microsoft/Resources.resx b/CredentialProvider.Microsoft/Resources.resx
index f43285aa..e4675553 100644
--- a/CredentialProvider.Microsoft/Resources.resx
+++ b/CredentialProvider.Microsoft/Resources.resx
@@ -214,6 +214,9 @@
[REDACTED]
+
+ [REDACTED_FEED_URL]
+
Request uri cannot be null
@@ -376,7 +379,13 @@ MSAL Token File Cache Enabled
Provide MSAL Cache Location
{29} (Preferred)
{30} (Legacy)
- Provide the location where the MSAL cache should be read and written to.
+ Provide the location where the MSAL cache should be read and written to.
+
+Redact Enabled
+ {31}
+ Boolean to enable redaction of passwords/tokens and feed URLs in output.
+ This is a best-effort feature. Always review logs before sharing.
+ Default is false. Can also be controlled via the -R command-line flag.
Failed to parse credentials
diff --git a/CredentialProvider.Microsoft/Util/EnvUtil.cs b/CredentialProvider.Microsoft/Util/EnvUtil.cs
index 47ab1605..ae634c56 100644
--- a/CredentialProvider.Microsoft/Util/EnvUtil.cs
+++ b/CredentialProvider.Microsoft/Util/EnvUtil.cs
@@ -56,6 +56,7 @@ public static class EnvUtil
public const string ProgramContext = "ARTIFACTS_CREDENTIALPROVIDER_PROGRAM_CONTEXT";
public const string MsalBrokerWindowEnvVar = "ARTIFACTS_CREDENTIALPROVIDER_MSAL_BROKER_WINDOW";
public const string EntraTokenOptInEnvVar = "ARTIFACTS_CREDENTIALPROVIDER_RETURN_ENTRA_TOKENS";
+ public const string RedactEnabledEnvVar = "ARTIFACTS_CREDENTIALPROVIDER_REDACT_ENABLED";
// Map of new environment variables to their legacy equivalents
private static readonly Dictionary EnvVarLegacyMap = new Dictionary
@@ -259,6 +260,16 @@ public static void SetProgramContextInEnvironment(Context context)
Environment.SetEnvironmentVariable(ProgramContext, context.ToString());
}
+ public static bool GetRedactionEnabledFromEnvironment()
+ {
+ var val = GetEnvironmentVariable(RedactEnabledEnvVar);
+ if (bool.TryParse(val, out bool result))
+ {
+ return result;
+ }
+ return false;
+ }
+
private static bool GetEnabledFromEnvironment(string artifactsVar, bool defaultValue = true)
{
var val = GetEnvironmentVariable(artifactsVar);
diff --git a/CredentialProvider.Microsoft/Util/RedactionUtil.cs b/CredentialProvider.Microsoft/Util/RedactionUtil.cs
new file mode 100644
index 00000000..3261a821
--- /dev/null
+++ b/CredentialProvider.Microsoft/Util/RedactionUtil.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft. All rights reserved.
+//
+// Licensed under the MIT license.
+
+using System;
+
+namespace NuGetCredentialProvider.Util
+{
+ ///
+ /// Utility class for redacting sensitive information in logs.
+ ///
+ public static class RedactionUtil
+ {
+ private static bool? shouldRedact;
+
+ public static bool ShouldRedact
+ {
+ get => shouldRedact ?? (shouldRedact = EnvUtil.GetRedactionEnabledFromEnvironment()).Value;
+ set => shouldRedact = value;
+ }
+
+ ///
+ /// Redacts a feed URL if redaction is enabled
+ ///
+ public static string RedactFeedUrl(Uri uri) =>
+ uri == null ? null : RedactFeedUrl(uri.AbsoluteUri);
+
+ ///
+ /// Redacts a feed URL if redaction is enabled
+ ///
+ public static string RedactFeedUrl(string uriString)
+ {
+ if (!ShouldRedact || string.IsNullOrEmpty(uriString))
+ {
+ return uriString;
+ }
+
+ try
+ {
+ var uri = new Uri(uriString);
+ return $"{uri.Scheme}://[REDACTED_FEED_URL]";
+ }
+ catch
+ {
+ return Resources.RedactedFeedUrl;
+ }
+ }
+
+ ///
+ /// Redacts a password/token if redaction is enabled
+ ///
+ public static string RedactPassword(string password) =>
+ !ShouldRedact || string.IsNullOrEmpty(password) ? password : Resources.Redacted;
+ }
+}
diff --git a/README.md b/README.md
index 949132d1..802bfd94 100644
--- a/README.md
+++ b/README.md
@@ -321,7 +321,8 @@ Verbosity (-V) Display this amount of detail in the output [Default='Info
Minimal
Warning
Error
-RedactPassword (-R) Prevents writing the password to standard output (for troubleshooting purposes)
+RedactPassword (-R) Redacts sensitive information from output: passwords/tokens and feed URLs. This is a best-effort
+ feature. Always review logs before sharing.
Help (-?, -h) Prints this help message
CanShowDialog (-C) If true, user can be prompted with credentials through UI, if false, device flow must be used [Default='True']
OutputFormat (-F) In standalone mode, format the results for human readability or as JSON. If JSON is selected, then logging (which may include Device
@@ -449,11 +450,15 @@ You can also capture credential provider logs directly to a file by setting the
```shell
# Windows (PowerShell)
$env:ARTIFACTS_CREDENTIALPROVIDER_LOG_PATH = "$PWD\credprovider.log"
+$env:ARTIFACTS_CREDENTIALPROVIDER_REDACT_ENABLED = "true" # Optional: redact sensitive info
# Linux/macOS
export ARTIFACTS_CREDENTIALPROVIDER_LOG_PATH="$PWD/credprovider.log"
+export ARTIFACTS_CREDENTIALPROVIDER_REDACT_ENABLED="true" # Optional: redact sensitive info
```
+> **Tip:** Set `ARTIFACTS_CREDENTIALPROVIDER_REDACT_ENABLED=true` to automatically redact passwords, tokens, and feed URLs from logs. This is a best-effort feature—always review logs before sharing to ensure no sensitive data is exposed.
+
#### How do I find out if my issue is a real 401?
Run the credential provider directly with the following command: `C:\Users\\.nuget\plugins\netfx\CredentialProvider.Microsoft\CredentialProvider.Microsoft.exe -I -V Verbose -U "https://pkgs.dev.azure.com/{organization}/{project-if-feed-is-project-scoped}/_packaging/{feed}/nuget/v3/index.json"`. Check you have the right permissions from the [feed permissions](https://docs.microsoft.com/en-us/azure/devops/artifacts/feeds/feed-permissions?view=azure-devops).