From 13ae83b0b6f1a44daf70e952f05c266f021a6735 Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Fri, 20 Feb 2026 23:58:50 -0800 Subject: [PATCH 1/5] Make ado token command environment-aware for SYSTEM_ACCESSTOKEN Refactor the ado token command to detect ADO Pipeline environments via the TF_BUILD env var and adjust SYSTEM_ACCESSTOKEN handling: - AZUREAUTH_ADO_PAT is always checked first (explicit user override) - In ADO Pipeline: use SYSTEM_ACCESSTOKEN if found, error if missing (interactive auth is not possible in pipelines) - Outside ADO Pipeline: warn if SYSTEM_ACCESSTOKEN is unexpectedly set, ignore it, and continue to normal AAD token auth Addresses #422 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Commands/Ado/CommandTokenTest.cs | 132 ++++++++++++++++++ src/AzureAuth.Test/IEnvExtensionsTest.cs | 14 ++ src/AzureAuth/Commands/Ado/CommandToken.cs | 39 +++++- src/AzureAuth/EnvVars.cs | 6 + src/AzureAuth/IEnvExtensions.cs | 10 ++ 5 files changed, 195 insertions(+), 6 deletions(-) diff --git a/src/AzureAuth.Test/Commands/Ado/CommandTokenTest.cs b/src/AzureAuth.Test/Commands/Ado/CommandTokenTest.cs index f13f8a8c..9875158a 100644 --- a/src/AzureAuth.Test/Commands/Ado/CommandTokenTest.cs +++ b/src/AzureAuth.Test/Commands/Ado/CommandTokenTest.cs @@ -3,13 +3,44 @@ namespace AzureAuth.Test.Commands.Ado { + using System; + using System.Collections.Generic; + using FluentAssertions; + using AzureAuth.Test; + using Microsoft.Authentication.AzureAuth; using Microsoft.Authentication.AzureAuth.Ado; using Microsoft.Authentication.AzureAuth.Commands.Ado; + using Microsoft.Authentication.MSALWrapper; + using Microsoft.Authentication.TestHelper; + using Microsoft.Extensions.Logging; + using Microsoft.IdentityModel.JsonWebTokens; + using Microsoft.Office.Lasso.Interfaces; + using Microsoft.Office.Lasso.Telemetry; + using Moq; + using NLog.Targets; using NUnit.Framework; internal class CommandTokenTest { + private Mock mockEnv; + private Mock mockTelemetry; + private Mock mockPublicClientAuth; + private ILogger logger; + private MemoryTarget logTarget; + private CommandExecuteEventData eventData; + + [SetUp] + public void SetUp() + { + this.mockEnv = new Mock(); + this.mockEnv.Setup(e => e.Get(It.IsAny())).Returns((string)null); + this.mockTelemetry = new Mock(); + this.mockPublicClientAuth = new Mock(); + (this.logger, this.logTarget) = MemoryLogger.Create(); + this.eventData = new CommandExecuteEventData(); + } + [TestCase("foobar", CommandToken.OutputMode.Token, "foobar")] [TestCase("foobar", CommandToken.OutputMode.HeaderValue, "Basic OmZvb2Jhcg==")] [TestCase("foobar", CommandToken.OutputMode.Header, "Authorization: Basic OmZvb2Jhcg==")] @@ -25,5 +56,106 @@ public void FormatToken_Bearer(string input, CommandToken.OutputMode mode, strin { CommandToken.FormatToken(input, mode, Authorization.Bearer).Should().Be(expected); } + + [Test] + public void OnExecute_AzureAuthAdoPat_AlwaysUsed() + { + this.mockEnv.Setup(e => e.Get("AZUREAUTH_ADO_PAT")).Returns("my-explicit-pat"); + + var command = new CommandToken(); + var result = command.OnExecute( + (ILogger)this.logger, + this.mockEnv.Object, + this.mockTelemetry.Object, + this.mockPublicClientAuth.Object, + this.eventData); + + result.Should().Be(0); + this.mockPublicClientAuth.Verify( + p => p.Token(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public void OnExecute_AdoPipeline_UsesSystemAccessToken() + { + this.mockEnv.Setup(e => e.Get("TF_BUILD")).Returns("True"); + this.mockEnv.Setup(e => e.Get("SYSTEM_ACCESSTOKEN")).Returns("pipeline-token"); + + var command = new CommandToken(); + var result = command.OnExecute( + (ILogger)this.logger, + this.mockEnv.Object, + this.mockTelemetry.Object, + this.mockPublicClientAuth.Object, + this.eventData); + + result.Should().Be(0); + this.mockPublicClientAuth.Verify( + p => p.Token(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public void OnExecute_AdoPipeline_NoSystemAccessToken_ReturnsError() + { + this.mockEnv.Setup(e => e.Get("TF_BUILD")).Returns("True"); + + var command = new CommandToken(); + var result = command.OnExecute( + (ILogger)this.logger, + this.mockEnv.Object, + this.mockTelemetry.Object, + this.mockPublicClientAuth.Object, + this.eventData); + + result.Should().Be(1); + this.logTarget.Logs.Should().Contain(l => l.Contains("SYSTEM_ACCESSTOKEN is not set")); + } + + [Test] + public void OnExecute_NotAdoPipeline_SystemAccessTokenSet_WarnsAndContinues() + { + this.mockEnv.Setup(e => e.Get("SYSTEM_ACCESSTOKEN")).Returns("stale-token"); + var fakeTokenResult = new TokenResult(new JsonWebToken(Fake.Token), Guid.NewGuid()); + this.mockPublicClientAuth + .Setup(p => p.Token(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(fakeTokenResult); + + var command = new CommandToken(); + var result = command.OnExecute( + (ILogger)this.logger, + this.mockEnv.Object, + this.mockTelemetry.Object, + this.mockPublicClientAuth.Object, + this.eventData); + + result.Should().Be(0); + this.logTarget.Logs.Should().Contain(l => l.Contains("does not appear to be an Azure DevOps Pipeline environment")); + + // Verify it fell through to AAD auth (ignored the SYSTEM_ACCESSTOKEN) + this.mockPublicClientAuth.Verify( + p => p.Token(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + + [Test] + public void OnExecute_AdoPipeline_AzureAuthAdoPat_TakesPriority() + { + this.mockEnv.Setup(e => e.Get("AZUREAUTH_ADO_PAT")).Returns("my-explicit-pat"); + this.mockEnv.Setup(e => e.Get("TF_BUILD")).Returns("True"); + this.mockEnv.Setup(e => e.Get("SYSTEM_ACCESSTOKEN")).Returns("pipeline-token"); + + var command = new CommandToken(); + var result = command.OnExecute( + (ILogger)this.logger, + this.mockEnv.Object, + this.mockTelemetry.Object, + this.mockPublicClientAuth.Object, + this.eventData); + + result.Should().Be(0); + this.logTarget.Logs.Should().Contain(l => l.Contains("AZUREAUTH_ADO_PAT")); + } } } diff --git a/src/AzureAuth.Test/IEnvExtensionsTest.cs b/src/AzureAuth.Test/IEnvExtensionsTest.cs index 3acb16ab..11ae52bd 100644 --- a/src/AzureAuth.Test/IEnvExtensionsTest.cs +++ b/src/AzureAuth.Test/IEnvExtensionsTest.cs @@ -58,6 +58,20 @@ public void InteractiveAuth_IsEnabledIfEnvVarsAreNotSet() IEnvExtensions.InteractiveAuthDisabled(this.envMock.Object).Should().BeFalse(); } + [TestCase("True", true)] + [TestCase("true", true)] + [TestCase("TRUE", true)] + [TestCase("False", false)] + [TestCase("", false)] + [TestCase(null, false)] + public void IsAdoPipeline_DetectsTfBuildEnvVar(string tfBuild, bool expected) + { + this.envMock.Setup(env => env.Get(It.IsAny())).Returns((string)null); + this.envMock.Setup(e => e.Get("TF_BUILD")).Returns(tfBuild); + + IEnvExtensions.IsAdoPipeline(this.envMock.Object).Should().Be(expected); + } + [Test] public void ReadAuthModeFromEnvOrSetDefault_ReturnsDefault_WhenEnvVarIsEmpty() { diff --git a/src/AzureAuth/Commands/Ado/CommandToken.cs b/src/AzureAuth/Commands/Ado/CommandToken.cs index 463bad73..3bc74469 100644 --- a/src/AzureAuth/Commands/Ado/CommandToken.cs +++ b/src/AzureAuth/Commands/Ado/CommandToken.cs @@ -8,7 +8,6 @@ namespace Microsoft.Authentication.AzureAuth.Commands.Ado using System.Linq; using McMaster.Extensions.CommandLineUtils; - using Microsoft.Authentication.AzureAuth.Ado; using Microsoft.Authentication.MSALWrapper; using Microsoft.Extensions.Logging; using Microsoft.Office.Lasso.Interfaces; @@ -89,15 +88,43 @@ public enum OutputMode /// An integer status code. 0 for success and non-zero for failure. public int OnExecute(ILogger logger, IEnv env, ITelemetryService telemetryService, IPublicClientAuth publicClientAuth, CommandExecuteEventData eventData) { - // First attempt using a PAT. - var pat = PatFromEnv.Get(env); - if (pat.Exists) + // Always check AZUREAUTH_ADO_PAT first - this is an explicit user override. + var adoPat = env.Get(EnvVars.AdoPat); + if (!string.IsNullOrEmpty(adoPat)) { - logger.LogDebug($"Using PAT from env var {pat.EnvVarSource}"); - logger.LogInformation(FormatToken(pat.Value, this.Output, Authorization.Basic)); + logger.LogDebug($"Using PAT from env var {EnvVars.AdoPat}"); + logger.LogInformation(FormatToken(adoPat, this.Output, Authorization.Basic)); return 0; } + // Check if we're in an ADO Pipeline environment. + bool isAdoPipeline = env.IsAdoPipeline(); + var systemAccessToken = env.Get(EnvVars.SystemAccessToken); + + if (isAdoPipeline) + { + if (!string.IsNullOrEmpty(systemAccessToken)) + { + logger.LogDebug($"Using token from env var {EnvVars.SystemAccessToken}"); + logger.LogInformation(FormatToken(systemAccessToken, this.Output, Authorization.Basic)); + return 0; + } + else + { + logger.LogError( + $"Running in an Azure DevOps Pipeline environment but {EnvVars.SystemAccessToken} is not set. " + + "Interactive authentication is not possible in a pipeline. " + + "Ensure the pipeline has access to the system token."); + return 1; + } + } + else if (!string.IsNullOrEmpty(systemAccessToken)) + { + logger.LogWarning( + $"{EnvVars.SystemAccessToken} is set but this does not appear to be an Azure DevOps Pipeline environment. " + + "Having this variable set on a developer machine is unusual. It will be ignored."); + } + // If command line options for mode are not specified, then use the environment variables. this.AuthModes ??= env.ReadAuthModeFromEnvOrSetDefault(); if (!this.AuthModes.Any()) diff --git a/src/AzureAuth/EnvVars.cs b/src/AzureAuth/EnvVars.cs index 4b439e0b..3842c84f 100644 --- a/src/AzureAuth/EnvVars.cs +++ b/src/AzureAuth/EnvVars.cs @@ -36,6 +36,12 @@ public static class EnvVars /// public static readonly string NoUser = $"{EnvVarPrefix}_NO_USER"; + /// + /// Name of the env var set by Azure DevOps Pipelines to indicate a pipeline environment. + /// Value is "True" when running in an ADO Pipeline. + /// + public const string TfBuild = "TF_BUILD"; + /// /// Name of the env var for the Azure DevOps pipelines default personal access token. /// diff --git a/src/AzureAuth/IEnvExtensions.cs b/src/AzureAuth/IEnvExtensions.cs index f67f4143..61dc10ff 100644 --- a/src/AzureAuth/IEnvExtensions.cs +++ b/src/AzureAuth/IEnvExtensions.cs @@ -16,6 +16,16 @@ public static class IEnvExtensions { private const string CorextPositiveValue = "1"; + /// + /// Determines whether we are running in an Azure DevOps Pipeline environment. + /// + /// The to use to get environment variables. + /// True if running in an Azure DevOps Pipeline. + public static bool IsAdoPipeline(this IEnv env) + { + return string.Equals("True", env.Get(EnvVars.TfBuild), StringComparison.OrdinalIgnoreCase); + } + /// /// Determines whether interactive auth is disabled or not. /// From 451d31d8069fe331365b6446ad76a9ee77cd67db Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Sat, 21 Feb 2026 15:17:47 -0800 Subject: [PATCH 2/5] Clean up a lot --- nuget.config | 8 +++ .../Commands/Ado/CommandTokenTest.cs | 60 ++++++++++++------- src/AzureAuth.Test/IEnvExtensionsTest.cs | 2 +- src/AzureAuth/Commands/Ado/CommandToken.cs | 1 + 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/nuget.config b/nuget.config index 6338e895..a3246f9f 100644 --- a/nuget.config +++ b/nuget.config @@ -4,4 +4,12 @@ + + + + + + + \ No newline at end of file diff --git a/src/AzureAuth.Test/Commands/Ado/CommandTokenTest.cs b/src/AzureAuth.Test/Commands/Ado/CommandTokenTest.cs index 9875158a..e92e0176 100644 --- a/src/AzureAuth.Test/Commands/Ado/CommandTokenTest.cs +++ b/src/AzureAuth.Test/Commands/Ado/CommandTokenTest.cs @@ -9,15 +9,15 @@ namespace AzureAuth.Test.Commands.Ado using FluentAssertions; using AzureAuth.Test; using Microsoft.Authentication.AzureAuth; - using Microsoft.Authentication.AzureAuth.Ado; using Microsoft.Authentication.AzureAuth.Commands.Ado; using Microsoft.Authentication.MSALWrapper; - using Microsoft.Authentication.TestHelper; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.Office.Lasso.Interfaces; using Microsoft.Office.Lasso.Telemetry; using Moq; + using NLog.Extensions.Logging; using NLog.Targets; using NUnit.Framework; @@ -26,7 +26,7 @@ internal class CommandTokenTest private Mock mockEnv; private Mock mockTelemetry; private Mock mockPublicClientAuth; - private ILogger logger; + private IServiceProvider serviceProvider; private MemoryTarget logTarget; private CommandExecuteEventData eventData; @@ -37,8 +37,26 @@ public void SetUp() this.mockEnv.Setup(e => e.Get(It.IsAny())).Returns((string)null); this.mockTelemetry = new Mock(); this.mockPublicClientAuth = new Mock(); - (this.logger, this.logTarget) = MemoryLogger.Create(); this.eventData = new CommandExecuteEventData(); + + // Setup in memory logging target with NLog - allows making assertions against what has been logged. + var loggingConfig = new NLog.Config.LoggingConfiguration(); + this.logTarget = new MemoryTarget("memory_target") + { + Layout = "${message}" // Define a simple layout so we don't get timestamps in messages. + }; + loggingConfig.AddTarget(this.logTarget); + loggingConfig.AddRuleForAllLevels(this.logTarget); + + // Setup Dependency Injection container to provide logger. + this.serviceProvider = new ServiceCollection() + .AddLogging(loggingBuilder => + { + loggingBuilder.ClearProviders(); + loggingBuilder.SetMinimumLevel(LogLevel.Trace); + loggingBuilder.AddNLog(loggingConfig); + }) + .BuildServiceProvider(); } [TestCase("foobar", CommandToken.OutputMode.Token, "foobar")] @@ -46,7 +64,7 @@ public void SetUp() [TestCase("foobar", CommandToken.OutputMode.Header, "Authorization: Basic OmZvb2Jhcg==")] public void FormatToken_Basic(string input, CommandToken.OutputMode mode, string expected) { - CommandToken.FormatToken(input, mode, Authorization.Basic).Should().Be(expected); + CommandToken.FormatToken(input, mode, Microsoft.Authentication.AzureAuth.Ado.Authorization.Basic).Should().Be(expected); } [TestCase("foobar", CommandToken.OutputMode.Token, "foobar")] @@ -54,17 +72,17 @@ public void FormatToken_Basic(string input, CommandToken.OutputMode mode, string [TestCase("foobar", CommandToken.OutputMode.Header, "Authorization: Bearer foobar")] public void FormatToken_Bearer(string input, CommandToken.OutputMode mode, string expected) { - CommandToken.FormatToken(input, mode, Authorization.Bearer).Should().Be(expected); + CommandToken.FormatToken(input, mode, Microsoft.Authentication.AzureAuth.Ado.Authorization.Bearer).Should().Be(expected); } [Test] public void OnExecute_AzureAuthAdoPat_AlwaysUsed() { - this.mockEnv.Setup(e => e.Get("AZUREAUTH_ADO_PAT")).Returns("my-explicit-pat"); + this.mockEnv.Setup(e => e.Get(EnvVars.AdoPat)).Returns("my-explicit-pat"); var command = new CommandToken(); var result = command.OnExecute( - (ILogger)this.logger, + this.serviceProvider.GetService>(), this.mockEnv.Object, this.mockTelemetry.Object, this.mockPublicClientAuth.Object, @@ -79,12 +97,12 @@ public void OnExecute_AzureAuthAdoPat_AlwaysUsed() [Test] public void OnExecute_AdoPipeline_UsesSystemAccessToken() { - this.mockEnv.Setup(e => e.Get("TF_BUILD")).Returns("True"); - this.mockEnv.Setup(e => e.Get("SYSTEM_ACCESSTOKEN")).Returns("pipeline-token"); + this.mockEnv.Setup(e => e.Get(EnvVars.TfBuild)).Returns("True"); + this.mockEnv.Setup(e => e.Get(EnvVars.SystemAccessToken)).Returns("pipeline-token"); var command = new CommandToken(); var result = command.OnExecute( - (ILogger)this.logger, + this.serviceProvider.GetService>(), this.mockEnv.Object, this.mockTelemetry.Object, this.mockPublicClientAuth.Object, @@ -99,24 +117,24 @@ public void OnExecute_AdoPipeline_UsesSystemAccessToken() [Test] public void OnExecute_AdoPipeline_NoSystemAccessToken_ReturnsError() { - this.mockEnv.Setup(e => e.Get("TF_BUILD")).Returns("True"); + this.mockEnv.Setup(e => e.Get(EnvVars.TfBuild)).Returns("True"); var command = new CommandToken(); var result = command.OnExecute( - (ILogger)this.logger, + this.serviceProvider.GetService>(), this.mockEnv.Object, this.mockTelemetry.Object, this.mockPublicClientAuth.Object, this.eventData); result.Should().Be(1); - this.logTarget.Logs.Should().Contain(l => l.Contains("SYSTEM_ACCESSTOKEN is not set")); + this.logTarget.Logs.Should().Contain(l => l.Contains($"{EnvVars.SystemAccessToken} is not set")); } [Test] public void OnExecute_NotAdoPipeline_SystemAccessTokenSet_WarnsAndContinues() { - this.mockEnv.Setup(e => e.Get("SYSTEM_ACCESSTOKEN")).Returns("stale-token"); + this.mockEnv.Setup(e => e.Get(EnvVars.SystemAccessToken)).Returns("stale-token"); var fakeTokenResult = new TokenResult(new JsonWebToken(Fake.Token), Guid.NewGuid()); this.mockPublicClientAuth .Setup(p => p.Token(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) @@ -124,7 +142,7 @@ public void OnExecute_NotAdoPipeline_SystemAccessTokenSet_WarnsAndContinues() var command = new CommandToken(); var result = command.OnExecute( - (ILogger)this.logger, + this.serviceProvider.GetService>(), this.mockEnv.Object, this.mockTelemetry.Object, this.mockPublicClientAuth.Object, @@ -142,20 +160,20 @@ public void OnExecute_NotAdoPipeline_SystemAccessTokenSet_WarnsAndContinues() [Test] public void OnExecute_AdoPipeline_AzureAuthAdoPat_TakesPriority() { - this.mockEnv.Setup(e => e.Get("AZUREAUTH_ADO_PAT")).Returns("my-explicit-pat"); - this.mockEnv.Setup(e => e.Get("TF_BUILD")).Returns("True"); - this.mockEnv.Setup(e => e.Get("SYSTEM_ACCESSTOKEN")).Returns("pipeline-token"); + this.mockEnv.Setup(e => e.Get(EnvVars.AdoPat)).Returns("my-explicit-pat"); + this.mockEnv.Setup(e => e.Get(EnvVars.TfBuild)).Returns("True"); + this.mockEnv.Setup(e => e.Get(EnvVars.SystemAccessToken)).Returns("pipeline-token"); var command = new CommandToken(); var result = command.OnExecute( - (ILogger)this.logger, + this.serviceProvider.GetService>(), this.mockEnv.Object, this.mockTelemetry.Object, this.mockPublicClientAuth.Object, this.eventData); result.Should().Be(0); - this.logTarget.Logs.Should().Contain(l => l.Contains("AZUREAUTH_ADO_PAT")); + this.logTarget.Logs.Should().Contain(l => l.Contains(EnvVars.AdoPat)); } } } diff --git a/src/AzureAuth.Test/IEnvExtensionsTest.cs b/src/AzureAuth.Test/IEnvExtensionsTest.cs index 11ae52bd..2ef4b938 100644 --- a/src/AzureAuth.Test/IEnvExtensionsTest.cs +++ b/src/AzureAuth.Test/IEnvExtensionsTest.cs @@ -67,7 +67,7 @@ public void InteractiveAuth_IsEnabledIfEnvVarsAreNotSet() public void IsAdoPipeline_DetectsTfBuildEnvVar(string tfBuild, bool expected) { this.envMock.Setup(env => env.Get(It.IsAny())).Returns((string)null); - this.envMock.Setup(e => e.Get("TF_BUILD")).Returns(tfBuild); + this.envMock.Setup(e => e.Get(EnvVars.TfBuild)).Returns(tfBuild); IEnvExtensions.IsAdoPipeline(this.envMock.Object).Should().Be(expected); } diff --git a/src/AzureAuth/Commands/Ado/CommandToken.cs b/src/AzureAuth/Commands/Ado/CommandToken.cs index 3bc74469..dd16082b 100644 --- a/src/AzureAuth/Commands/Ado/CommandToken.cs +++ b/src/AzureAuth/Commands/Ado/CommandToken.cs @@ -8,6 +8,7 @@ namespace Microsoft.Authentication.AzureAuth.Commands.Ado using System.Linq; using McMaster.Extensions.CommandLineUtils; + using Microsoft.Authentication.AzureAuth.Ado; using Microsoft.Authentication.MSALWrapper; using Microsoft.Extensions.Logging; using Microsoft.Office.Lasso.Interfaces; From fb2c6943b665e6b15e7909fa8222a869b33b6e5b Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Sat, 21 Feb 2026 15:43:52 -0800 Subject: [PATCH 3/5] Refactor logic --- src/AzureAuth/Ado/PatFromEnv.cs | 8 +--- src/AzureAuth/Commands/Ado/CommandToken.cs | 45 +++++++++------------- 2 files changed, 19 insertions(+), 34 deletions(-) diff --git a/src/AzureAuth/Ado/PatFromEnv.cs b/src/AzureAuth/Ado/PatFromEnv.cs index f02c2d48..4be4b78e 100644 --- a/src/AzureAuth/Ado/PatFromEnv.cs +++ b/src/AzureAuth/Ado/PatFromEnv.cs @@ -3,18 +3,12 @@ namespace Microsoft.Authentication.AzureAuth.Ado { - using System; using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using Microsoft.Authentication.MSALWrapper; - using Microsoft.Authentication.MSALWrapper.AuthFlow; - using Microsoft.Extensions.Logging; using Microsoft.Office.Lasso.Interfaces; /// - /// A class for getting an ADO PAT from an or an AAD access token through MSAL. + /// A class for getting an ADO PAT from an . /// public static class PatFromEnv { diff --git a/src/AzureAuth/Commands/Ado/CommandToken.cs b/src/AzureAuth/Commands/Ado/CommandToken.cs index dd16082b..2244cb00 100644 --- a/src/AzureAuth/Commands/Ado/CommandToken.cs +++ b/src/AzureAuth/Commands/Ado/CommandToken.cs @@ -89,41 +89,32 @@ public enum OutputMode /// An integer status code. 0 for success and non-zero for failure. public int OnExecute(ILogger logger, IEnv env, ITelemetryService telemetryService, IPublicClientAuth publicClientAuth, CommandExecuteEventData eventData) { - // Always check AZUREAUTH_ADO_PAT first - this is an explicit user override. - var adoPat = env.Get(EnvVars.AdoPat); - if (!string.IsNullOrEmpty(adoPat)) + // First attempt using a PAT from the environment. + var pat = PatFromEnv.Get(env); + if (pat.Exists) { - logger.LogDebug($"Using PAT from env var {EnvVars.AdoPat}"); - logger.LogInformation(FormatToken(adoPat, this.Output, Authorization.Basic)); - return 0; - } - - // Check if we're in an ADO Pipeline environment. - bool isAdoPipeline = env.IsAdoPipeline(); - var systemAccessToken = env.Get(EnvVars.SystemAccessToken); - - if (isAdoPipeline) - { - if (!string.IsNullOrEmpty(systemAccessToken)) + // SYSTEM_ACCESSTOKEN should only be used inside an ADO Pipeline. + if (pat.EnvVarSource == EnvVars.SystemAccessToken && !env.IsAdoPipeline()) { - logger.LogDebug($"Using token from env var {EnvVars.SystemAccessToken}"); - logger.LogInformation(FormatToken(systemAccessToken, this.Output, Authorization.Basic)); - return 0; + logger.LogWarning( + $"{EnvVars.SystemAccessToken} is set but this does not appear to be an Azure DevOps Pipeline environment. " + + "Having this variable set on a developer machine is unusual. It will be ignored."); } else { - logger.LogError( - $"Running in an Azure DevOps Pipeline environment but {EnvVars.SystemAccessToken} is not set. " - + "Interactive authentication is not possible in a pipeline. " - + "Ensure the pipeline has access to the system token."); - return 1; + logger.LogDebug($"Using PAT from env var {pat.EnvVarSource}"); + logger.LogInformation(FormatToken(pat.Value, this.Output, Authorization.Basic)); + return 0; } } - else if (!string.IsNullOrEmpty(systemAccessToken)) + else if (env.IsAdoPipeline()) { - logger.LogWarning( - $"{EnvVars.SystemAccessToken} is set but this does not appear to be an Azure DevOps Pipeline environment. " - + "Having this variable set on a developer machine is unusual. It will be ignored."); + // In a pipeline but no token was found at all. + logger.LogError( + $"Running in an Azure DevOps Pipeline environment but {EnvVars.SystemAccessToken} is not set. " + + "Interactive authentication is not possible in a pipeline. " + + "Ensure the pipeline has access to the system token."); + return 1; } // If command line options for mode are not specified, then use the environment variables. From f06ded384144de1495fd2e0001a042ca2af0d530 Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Sat, 21 Feb 2026 15:44:56 -0800 Subject: [PATCH 4/5] Fix warnings --- src/AzureAuth.Test/IEnvExtensionsTest.cs | 1 - src/AzureAuth/IEnvExtensions.cs | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/AzureAuth.Test/IEnvExtensionsTest.cs b/src/AzureAuth.Test/IEnvExtensionsTest.cs index 2ef4b938..86ce993b 100644 --- a/src/AzureAuth.Test/IEnvExtensionsTest.cs +++ b/src/AzureAuth.Test/IEnvExtensionsTest.cs @@ -10,7 +10,6 @@ namespace AzureAuth.Test using Microsoft.Authentication.TestHelper; using Microsoft.Extensions.Logging; using Microsoft.Office.Lasso.Interfaces; - using Microsoft.Office.Lasso.Telemetry; using Moq; using NLog.Targets; using NUnit.Framework; diff --git a/src/AzureAuth/IEnvExtensions.cs b/src/AzureAuth/IEnvExtensions.cs index 61dc10ff..ad214a6c 100644 --- a/src/AzureAuth/IEnvExtensions.cs +++ b/src/AzureAuth/IEnvExtensions.cs @@ -5,7 +5,6 @@ namespace Microsoft.Authentication.AzureAuth { using Microsoft.Authentication.MSALWrapper; using Microsoft.Office.Lasso.Interfaces; - using Microsoft.Office.Lasso.Telemetry; using System.Collections.Generic; using System; @@ -41,7 +40,6 @@ public static bool InteractiveAuthDisabled(this IEnv env) /// Get the auth modes from the environment or set the default. /// /// The to use. - /// Event data to add the auth mode to. /// AuthModes. public static IEnumerable ReadAuthModeFromEnvOrSetDefault(this IEnv env) { From 2715a424fd79c9a78d8c0829665438b1561db9fd Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Sat, 21 Feb 2026 15:52:38 -0800 Subject: [PATCH 5/5] Fix build warning --- src/AzureAuth/Commands/Ado/Pat/CommandScopes.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AzureAuth/Commands/Ado/Pat/CommandScopes.cs b/src/AzureAuth/Commands/Ado/Pat/CommandScopes.cs index bbf0171a..aa9bdb69 100644 --- a/src/AzureAuth/Commands/Ado/Pat/CommandScopes.cs +++ b/src/AzureAuth/Commands/Ado/Pat/CommandScopes.cs @@ -10,8 +10,7 @@ namespace Microsoft.Authentication.AzureAuth.Commands.Ado.Pat { /// - /// Command to print the list of available scopes - + /// Command to print the list of available scopes for ADO PATs. /// [Command("scopes", Description = "List the valid ado pat scopes")] public class CommandScopes