Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions nuget.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@
<clear />
<add key="Office" value="https://pkgs.dev.azure.com/office/_packaging/Office/nuget/v3/index.json"/>
</packageSources>
<!-- Provide ADO_TOKEN for authenticated package restore:
ADO_TOKEN=$(azureauth ado token) dotnet restore -->
<packageSourceCredentials>
<Office>
<add key="Username" value="UnusedWithToken" />
<add key="ClearTextPassword" value="%ADO_TOKEN%" />
</Office>
</packageSourceCredentials>
</configuration>
156 changes: 153 additions & 3 deletions src/AzureAuth.Test/Commands/Ado/CommandTokenTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,177 @@

namespace AzureAuth.Test.Commands.Ado
{
using System;
using System.Collections.Generic;

using FluentAssertions;
using Microsoft.Authentication.AzureAuth.Ado;
using AzureAuth.Test;
using Microsoft.Authentication.AzureAuth;
using Microsoft.Authentication.AzureAuth.Commands.Ado;
using Microsoft.Authentication.MSALWrapper;
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;

internal class CommandTokenTest
{
private Mock<IEnv> mockEnv;
private Mock<ITelemetryService> mockTelemetry;
private Mock<IPublicClientAuth> mockPublicClientAuth;
private IServiceProvider serviceProvider;
private MemoryTarget logTarget;
private CommandExecuteEventData eventData;

[SetUp]
public void SetUp()
{
this.mockEnv = new Mock<IEnv>();
this.mockEnv.Setup(e => e.Get(It.IsAny<string>())).Returns((string)null);
this.mockTelemetry = new Mock<ITelemetryService>();
this.mockPublicClientAuth = new Mock<IPublicClientAuth>();
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")]
[TestCase("foobar", CommandToken.OutputMode.HeaderValue, "Basic OmZvb2Jhcg==")]
[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")]
[TestCase("foobar", CommandToken.OutputMode.HeaderValue, "Bearer foobar")]
[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(EnvVars.AdoPat)).Returns("my-explicit-pat");

var command = new CommandToken();
var result = command.OnExecute(
this.serviceProvider.GetService<ILogger<CommandToken>>(),
this.mockEnv.Object,
this.mockTelemetry.Object,
this.mockPublicClientAuth.Object,
this.eventData);

result.Should().Be(0);
this.mockPublicClientAuth.Verify(
p => p.Token(It.IsAny<AuthParameters>(), It.IsAny<IEnumerable<AuthMode>>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>(), It.IsAny<EventData>()),
Times.Never);
}

[Test]
public void OnExecute_AdoPipeline_UsesSystemAccessToken()
{
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(
this.serviceProvider.GetService<ILogger<CommandToken>>(),
this.mockEnv.Object,
this.mockTelemetry.Object,
this.mockPublicClientAuth.Object,
this.eventData);

result.Should().Be(0);
this.mockPublicClientAuth.Verify(
p => p.Token(It.IsAny<AuthParameters>(), It.IsAny<IEnumerable<AuthMode>>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>(), It.IsAny<EventData>()),
Times.Never);
}

[Test]
public void OnExecute_AdoPipeline_NoSystemAccessToken_ReturnsError()
{
this.mockEnv.Setup(e => e.Get(EnvVars.TfBuild)).Returns("True");

var command = new CommandToken();
var result = command.OnExecute(
this.serviceProvider.GetService<ILogger<CommandToken>>(),
this.mockEnv.Object,
this.mockTelemetry.Object,
this.mockPublicClientAuth.Object,
this.eventData);

result.Should().Be(1);
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(EnvVars.SystemAccessToken)).Returns("stale-token");
var fakeTokenResult = new TokenResult(new JsonWebToken(Fake.Token), Guid.NewGuid());
this.mockPublicClientAuth
.Setup(p => p.Token(It.IsAny<AuthParameters>(), It.IsAny<IEnumerable<AuthMode>>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>(), It.IsAny<EventData>()))
.Returns(fakeTokenResult);

var command = new CommandToken();
var result = command.OnExecute(
this.serviceProvider.GetService<ILogger<CommandToken>>(),
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<AuthParameters>(), It.IsAny<IEnumerable<AuthMode>>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<TimeSpan>(), It.IsAny<EventData>()),
Times.Once);
}

[Test]
public void OnExecute_AdoPipeline_AzureAuthAdoPat_TakesPriority()
{
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(
this.serviceProvider.GetService<ILogger<CommandToken>>(),
this.mockEnv.Object,
this.mockTelemetry.Object,
this.mockPublicClientAuth.Object,
this.eventData);

result.Should().Be(0);
this.logTarget.Logs.Should().Contain(l => l.Contains(EnvVars.AdoPat));
}
}
}
15 changes: 14 additions & 1 deletion src/AzureAuth.Test/IEnvExtensionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -58,6 +57,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<string>())).Returns((string)null);
this.envMock.Setup(e => e.Get(EnvVars.TfBuild)).Returns(tfBuild);

IEnvExtensions.IsAdoPipeline(this.envMock.Object).Should().Be(expected);
}

[Test]
public void ReadAuthModeFromEnvOrSetDefault_ReturnsDefault_WhenEnvVarIsEmpty()
{
Expand Down
8 changes: 1 addition & 7 deletions src/AzureAuth/Ado/PatFromEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// A class for getting an ADO PAT from an <see cref="IEnv"/> or an AAD access token through MSAL.
/// A class for getting an ADO PAT from an <see cref="IEnv"/>.
/// </summary>
public static class PatFromEnv
{
Expand Down
27 changes: 23 additions & 4 deletions src/AzureAuth/Commands/Ado/CommandToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,32 @@ public enum OutputMode
/// <returns>An integer status code. 0 for success and non-zero for failure.</returns>
public int OnExecute(ILogger<CommandToken> logger, IEnv env, ITelemetryService telemetryService, IPublicClientAuth publicClientAuth, CommandExecuteEventData eventData)
{
// First attempt using a PAT.
// First attempt using a PAT from the environment.
var pat = PatFromEnv.Get(env);
if (pat.Exists)
{
logger.LogDebug($"Using PAT from env var {pat.EnvVarSource}");
logger.LogInformation(FormatToken(pat.Value, this.Output, Authorization.Basic));
return 0;
// SYSTEM_ACCESSTOKEN should only be used inside an ADO Pipeline.
if (pat.EnvVarSource == EnvVars.SystemAccessToken && !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.");
}
else
{
logger.LogDebug($"Using PAT from env var {pat.EnvVarSource}");
logger.LogInformation(FormatToken(pat.Value, this.Output, Authorization.Basic));
return 0;
}
}
else if (env.IsAdoPipeline())
{
// 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.
Expand Down
3 changes: 1 addition & 2 deletions src/AzureAuth/Commands/Ado/Pat/CommandScopes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
namespace Microsoft.Authentication.AzureAuth.Commands.Ado.Pat
{
/// <summary>
/// Command to print the list of available scopes

/// Command to print the list of available scopes for ADO PATs.
/// </summary>
[Command("scopes", Description = "List the valid ado pat scopes")]
public class CommandScopes
Expand Down
6 changes: 6 additions & 0 deletions src/AzureAuth/EnvVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ public static class EnvVars
/// </summary>
public static readonly string NoUser = $"{EnvVarPrefix}_NO_USER";

/// <summary>
/// Name of the env var set by Azure DevOps Pipelines to indicate a pipeline environment.
/// Value is "True" when running in an ADO Pipeline.
/// </summary>
public const string TfBuild = "TF_BUILD";

/// <summary>
/// Name of the env var for the Azure DevOps pipelines default personal access token.
/// </summary>
Expand Down
12 changes: 10 additions & 2 deletions src/AzureAuth/IEnvExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -16,6 +15,16 @@ public static class IEnvExtensions
{
private const string CorextPositiveValue = "1";

/// <summary>
/// Determines whether we are running in an Azure DevOps Pipeline environment.
/// </summary>
/// <param name="env">The <see cref="IEnv"/> to use to get environment variables.</param>
/// <returns>True if running in an Azure DevOps Pipeline.</returns>
public static bool IsAdoPipeline(this IEnv env)
{
return string.Equals("True", env.Get(EnvVars.TfBuild), StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Determines whether interactive auth is disabled or not.
/// </summary>
Expand All @@ -31,7 +40,6 @@ public static bool InteractiveAuthDisabled(this IEnv env)
/// Get the auth modes from the environment or set the default.
/// </summary>
/// <param name="env">The <see cref="IEnv"/> to use.</param>
/// <param name="eventData">Event data to add the auth mode to.</param>
/// <returns>AuthModes.</returns>
public static IEnumerable<AuthMode> ReadAuthModeFromEnvOrSetDefault(this IEnv env)
{
Expand Down