Skip to content

Commit

Permalink
Not allowing VstsCredentialProvider to be used if the URL is on prem (#…
Browse files Browse the repository at this point in the history
…191)

* Not allowing VstsCredentialProvider to be used if the URL is on prem
  • Loading branch information
satbai authored Jun 11, 2020
1 parent 2011609 commit 3902a1e
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 35 deletions.
2 changes: 1 addition & 1 deletion Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<CredentialProviderVersion>0.1.22</CredentialProviderVersion>
<CredentialProviderVersion>0.1.23</CredentialProviderVersion>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public void TestInitialize()
public void TestCleanup()
{
Environment.SetEnvironmentVariable(EnvUtil.AuthorityEnvVar, string.Empty);
Environment.SetEnvironmentVariable(EnvUtil.PpeHostsEnvVar, string.Empty);
}

[TestMethod]
Expand Down Expand Up @@ -108,46 +109,69 @@ public async Task GetAadAuthorityUri_WithAuthenticateHeadersAndEnvironmentOverri
}

[TestMethod]
public async Task IsVstsUri_TenantHeaderNotPresent_ReturnsFalse()
public async Task GetFeedUriSource_TenantHeaderNotPresent_ReturnsExternal()
{
var requestUri = new Uri("https://example.pkgs.visualstudio.com/_packaging/feed/nuget/v3/index.json");

var isVstsUri = await authUtil.IsVstsUriAsync(requestUri);
isVstsUri.Should().BeFalse();
var feedSource = await authUtil.GetAzDevDeploymentType(requestUri);
feedSource.Should().Be(AzDevDeploymentType.External);
}

[TestMethod]
public async Task IsVstsUri_TenantHeaderPresent_ReturnsFalse()
public async Task GetFeedUriSource_TenantHeaderPresent_VssAuthorizationEndpointNotPresent_ReturnsExternal()
{
var requestUri = new Uri("https://example.pkgs.visualstudio.com/_packaging/feed/nuget/v3/index.json");

MockVssResourceTenantHeader();

var isVstsUri = await authUtil.IsVstsUriAsync(requestUri);
isVstsUri.Should().BeFalse();
var feedSource = await authUtil.GetAzDevDeploymentType(requestUri);
feedSource.Should().Be(AzDevDeploymentType.External);
}

[TestMethod]
public async Task IsVstsUri_AuthorizationEndpointHeaderPresent_ReturnsFalse()
public async Task GetFeedUriSource_AuthorizationEndpointHeaderPresent_TenantHeaderNotPresent_ReturnsExternal()
{
var requestUri = new Uri("https://example.pkgs.visualstudio.com/_packaging/feed/nuget/v3/index.json");

MockVssAuthorizationEndpointHeader();

var isVstsUri = await authUtil.IsVstsUriAsync(requestUri);
isVstsUri.Should().BeFalse();
var feedSource = await authUtil.GetAzDevDeploymentType(requestUri);
feedSource.Should().Be(AzDevDeploymentType.External);
}

[TestMethod]
public async Task IsVstsUri_BothHeadersPresent_ReturnsTrue()
public async Task GetFeedUriSource_BothHeadersPresent_ReturnsHosted()
{
var requestUri = new Uri("https://example.pkgs.visualstudio.com/_packaging/feed/nuget/v3/index.json");

MockVssResourceTenantHeader();
MockVssAuthorizationEndpointHeader();

var isVstsUri = await authUtil.IsVstsUriAsync(requestUri);
isVstsUri.Should().BeTrue();
var feedSource = await authUtil.GetAzDevDeploymentType(requestUri);
feedSource.Should().Be(AzDevDeploymentType.Hosted);
}

[TestMethod]
public async Task GetFeedUriSource_NoHttps_ReturnsExternal()
{
var requestUri = new Uri("http://example.pkgs.visualstudio.com/_packaging/feed/nuget/v3/index.json");

MockVssResourceTenantHeader();
MockVssAuthorizationEndpointHeader();

var feedSource = await authUtil.GetAzDevDeploymentType(requestUri);
feedSource.Should().Be(AzDevDeploymentType.External);
}

[TestMethod]
public async Task GetFeedUriSource_OnPremHeaderPresent_ReturnsOnPrem()
{
var requestUri = new Uri("https://example.pkgs.visualstudio.com/_packaging/feed/nuget/v3/index.json");

MockResponseHeaders(AuthUtil.VssE2EID, "id");

var feedSource = await authUtil.GetAzDevDeploymentType(requestUri);
feedSource.Should().Be(AzDevDeploymentType.OnPrem);
}

[TestMethod]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public async Task CanProvideCredentials_ReturnsTrueForKnownSources()
}

mockAuthUtil
.Verify(x => x.IsVstsUriAsync(It.IsAny<Uri>()), Times.Never, "because we shouldn't probe for known sources");
.Verify(x => x.GetAzDevDeploymentType(It.IsAny<Uri>()), Times.Never, "because we shouldn't probe for known sources");
}

[TestMethod]
Expand All @@ -103,7 +103,29 @@ public async Task CanProvideCredentials_ReturnsTrueForOverridenSources()
}

mockAuthUtil
.Verify(x => x.IsVstsUriAsync(It.IsAny<Uri>()), Times.Never, "because we shouldn't probe for known sources");
.Verify(x => x.GetAzDevDeploymentType(It.IsAny<Uri>()), Times.Never, "because we shouldn't probe for known sources");

Environment.SetEnvironmentVariable(EnvUtil.SupportedHostsEnvVar, string.Empty);
}

[TestMethod]
public async Task CanProvideCredentials_CallsGetFeedUriSourceWhenSourcesAreInvalid()
{
var sources = new[]
{
new Uri(@"http://example.overrideOne.com/_packaging/TestFeed/nuget/v3/index.json"),
new Uri(@"https://example.overrideTwo.com/_packaging/TestFeed/nuget/v3/index.json"),
new Uri(@"https://example.overrideThre.com/_packaging/TestFeed/nuget/v3/index.json"),
};

foreach (var source in sources)
{
var canProvideCredentials = await vstsCredentialProvider.CanProvideCredentialsAsync(source);
canProvideCredentials.Should().BeFalse($"because {source} is not a valid host");
}

mockAuthUtil
.Verify(x => x.GetAzDevDeploymentType(It.IsAny<Uri>()), Times.Exactly(3), "because sources were unknown");
}

[TestMethod]
Expand Down
36 changes: 26 additions & 10 deletions CredentialProvider.Microsoft/CredentialProviders/Vsts/IAuthUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,24 @@ public interface IAuthUtil
{
Task<Uri> GetAadAuthorityUriAsync(Uri uri, CancellationToken cancellationToken);

Task<bool> IsVstsUriAsync(Uri uri);
Task<AzDevDeploymentType> GetAzDevDeploymentType(Uri uri);

Task<Uri> GetAuthorizationEndpoint(Uri uri, CancellationToken cancellationToken);
}

public enum AzDevDeploymentType
{
External,
Hosted,
OnPrem
}

public class AuthUtil : IAuthUtil
{
public const string VssResourceTenant = "X-VSS-ResourceTenant";
public const string VssAuthorizationEndpoint = "X-VSS-AuthorizationEndpoint";

public const string VssE2EID = "X-VSS-E2EID";

private readonly ILogger logger;

public AuthUtil(ILogger logger)
Expand All @@ -44,7 +52,7 @@ public async Task<Uri> GetAadAuthorityUriAsync(Uri uri, CancellationToken cancel

var headers = await GetResponseHeadersAsync(uri, cancellationToken);
var bearerHeaders = headers.WwwAuthenticate.Where(x => x.Scheme.Equals("Bearer", StringComparison.Ordinal));

foreach (var param in bearerHeaders)
{
if (param.Parameter == null)
Expand Down Expand Up @@ -73,17 +81,25 @@ public async Task<Uri> GetAadAuthorityUriAsync(Uri uri, CancellationToken cancel
return new Uri($"{aadBase}/common");
}

public async Task<bool> IsVstsUriAsync(Uri uri)
public async Task<AzDevDeploymentType> GetAzDevDeploymentType(Uri uri)
{
if (!IsValidScheme(uri))
// Ping the url to see from headers whether it's an Azure Artifacts feed or external
var responseHeaders = await GetResponseHeadersAsync(uri, cancellationToken: default);

// Hosted only allows https
if (IsHttpsScheme(uri) && responseHeaders.Contains(VssResourceTenant) && responseHeaders.Contains(VssAuthorizationEndpoint))
{
// We are not talking to a https endpoint so it cannot be a VSTS endpoint
return false;
return AzDevDeploymentType.Hosted;
}

var responseHeaders = await GetResponseHeadersAsync(uri, cancellationToken: default);
// If not hosted and has E2EID, assume on prem.
if (responseHeaders.Contains(VssE2EID))
{
return AzDevDeploymentType.OnPrem;
}

return responseHeaders.Contains(VssResourceTenant) && responseHeaders.Contains(VssAuthorizationEndpoint);
// Assume uri is from an external source if expected headers aren't present.
return AzDevDeploymentType.External;
}

public async Task<Uri> GetAuthorizationEndpoint(Uri uri, CancellationToken cancellationToken)
Expand Down Expand Up @@ -141,7 +157,7 @@ private bool UsePpeAadUrl(Uri uri)
return ppeHosts.Any(host => uri.Host.EndsWith(host, StringComparison.OrdinalIgnoreCase));
}

private bool IsValidScheme(Uri uri)
private bool IsHttpsScheme(Uri uri)
{
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,37 @@ public override async Task<bool> CanProvideCredentialsAsync(Uri uri)
return false;
}

var validHosts = EnvUtil.GetHostsFromEnvironment(Logger, EnvUtil.SupportedHostsEnvVar, new[]
var validHosts = EnvUtil.GetHostsFromEnvironment(Logger, EnvUtil.SupportedHostsEnvVar, new[]
{
".pkgs.vsts.me", // DevFabric
".pkgs.codedev.ms", // DevFabric
".pkgs.codeapp.ms", // AppFabric
".pkgs.visualstudio.com", // Prod
".pkgs.dev.azure.com" // Prod
});

bool isValidHost = validHosts.Any(host => uri.Host.EndsWith(host, StringComparison.OrdinalIgnoreCase));
if (isValidHost)
{
".pkgs.vsts.me", // DevFabric
".pkgs.codedev.ms", // DevFabric
".pkgs.codeapp.ms", // AppFabric
".pkgs.visualstudio.com", // Prod
".pkgs.dev.azure.com" // Prod
});

return validHosts.Any(host => uri.Host.EndsWith(host, StringComparison.OrdinalIgnoreCase)) || await authUtil.IsVstsUriAsync(uri);
Verbose(string.Format(Resources.HostAccepted, uri.Host));
return true;
}

var azDevOpsType = await authUtil.GetAzDevDeploymentType(uri);
if (azDevOpsType == AzDevDeploymentType.Hosted)
{
Verbose(Resources.ValidHeaders);
return true;
}

if (azDevOpsType == AzDevDeploymentType.OnPrem)
{
Verbose(Resources.OnPremDetected);
return false;
}

Verbose(string.Format(Resources.ExternalUri, uri));
return false;
}

public override async Task<GetAuthenticationCredentialsResponse> HandleRequestAsync(GetAuthenticationCredentialsRequest request, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public override async Task<GetAuthenticationCredentialsResponse> HandleRequestAs
{
Logger.Verbose(string.Format(Resources.SkippingCredentialProvider, credentialProvider, request.Uri.AbsoluteUri));
continue;
}
}
Logger.Verbose(string.Format(Resources.UsingCredentialProvider, credentialProvider, request.Uri.AbsoluteUri));

if (credentialProvider.IsCachable && TryCache(request, out string cachedToken))
{
Expand Down
45 changes: 45 additions & 0 deletions CredentialProvider.Microsoft/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions CredentialProvider.Microsoft/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -459,4 +459,20 @@ NuGet workarounds
<data name="UIFlowStarted" xml:space="preserve">
<value>Using the {0} flow for uri {1}. User sign-in required in a pop-up authentication window.</value>
</data>
<data name="HostAccepted" xml:space="preserve">
<value>Matched well-known Azure DevOps Service hostname: {0}</value>
</data>
<data name="OnPremDetected" xml:space="preserve">
<value>Detected an on premise Azure DevOps Server.</value>
</data>
<data name="UsingCredentialProvider" xml:space="preserve">
<value>Using {0} to try to get credentials for {1}.</value>
<comment>{0} - credential provider name, {1} - Uri</comment>
</data>
<data name="ValidHeaders" xml:space="preserve">
<value>Detected a hosted Azure DevOps Service.</value>
</data>
<data name="ExternalUri" xml:space="preserve">
<value>{0} is not an Azure Artifacts feed.</value>
</data>
</root>

0 comments on commit 3902a1e

Please sign in to comment.