Skip to content

Commit c805b11

Browse files
authored
Add Support for Symweb Authentication to PerfView (#2039)
* Add Symweb authentication support. Fix loading of dependencies for authentication and add special handling for symweb. * Update SymbolReader to only download headers before starting to display download progress.
1 parent a662474 commit c805b11

6 files changed

Lines changed: 199 additions & 7 deletions

File tree

src/Directory.Build.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<MicroBuildCoreVersion>0.3.1</MicroBuildCoreVersion>
4646
<MicrosoftIdentityClientVersion>4.60.1</MicrosoftIdentityClientVersion>
4747
<MicrosoftIdentityClientExtensionsMsalVersion>4.60.1</MicrosoftIdentityClientExtensionsMsalVersion>
48+
<MicrosoftIdentityModelAbstractionsVersion>7.1.2</MicrosoftIdentityModelAbstractionsVersion>
4849
<MicrosoftIdentityModelTokensVersion>7.1.2</MicrosoftIdentityModelTokensVersion>
4950
<MicrosoftIdentityModelJsonWebTokensVersion>7.1.2</MicrosoftIdentityModelJsonWebTokensVersion>
5051
<MicrosoftSourceLinkGitHubVersion>8.0.0</MicrosoftSourceLinkGitHubVersion>
@@ -58,6 +59,8 @@
5859
<SystemReflectionTypeExtensionsVersion>4.7.0</SystemReflectionTypeExtensionsVersion>
5960
<SystemRuntimeCompilerServicesUnsafeVersion>6.0.0</SystemRuntimeCompilerServicesUnsafeVersion>
6061
<SystemRuntimeInteropServicesRuntimeInformationVersion></SystemRuntimeInteropServicesRuntimeInformationVersion>
62+
<SystemSecurityCryptographyAlgorithmsVersion>4.3.1</SystemSecurityCryptographyAlgorithmsVersion>
63+
<SystemSecurityCryptographyProtectedDataVersion>4.7.0</SystemSecurityCryptographyProtectedDataVersion>
6164
<SystemTextEncodingsWebVersion>8.0.0</SystemTextEncodingsWebVersion>
6265
<SystemTextJsonVersion>8.0.1</SystemTextJsonVersion>
6366
<SystemThreadingTasksExtensionsVersion>4.5.4</SystemThreadingTasksExtensionsVersion>

src/PerfView/Authentication.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ public static void Configure(this SymbolReaderHttpHandler handler, Authenticatio
214214

215215
handler.ClearHandlers();
216216

217+
// Always add Symweb authentication.
218+
handler.AddSymwebAuthentication(log);
219+
217220
// The order isn't critical, but we chose to put GCM last
218221
// because the user might want to use GCM for GitHub and
219222
// Developer identity for Azure DevOps. If GCM were first,

src/PerfView/PerfView.csproj

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
<NeutralLanguage>en</NeutralLanguage>
3838

3939
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
40-
4140
<NuspecFile>PerfView.nuspec</NuspecFile>
4241
<GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);SetNuspecProperties</GenerateNuspecDependsOn>
4342
</PropertyGroup>
@@ -87,11 +86,13 @@
8786
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent.SupportFiles" Version="$(MicrosoftDiagnosticsTracingTraceEventSupportFilesVersion)" PrivateAssets="all" />
8887
<PackageReference Include="Microsoft.Identity.Client" Version="$(MicrosoftIdentityClientVersion)" GeneratePathProperty="true" />
8988
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="$(MicrosoftIdentityClientExtensionsMsalVersion)" GeneratePathProperty="true" />
89+
<PackageReference Include="Microsoft.IdentityModel.Abstractions" Version="$(MicrosoftIdentityModelAbstractionsVersion)" GeneratePathProperty="true" />
9090
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="$(MicrosoftIdentityModelTokensVersion)" GeneratePathProperty="true" />
9191
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="$(MicrosoftIdentityModelJsonWebTokensVersion)" GeneratePathProperty="true" />
9292
<PackageReference Include="PerfView.SupportFiles" Version="$(PerfViewSupportFilesVersion)" PrivateAssets="all" />
9393
<PackageReference Include="System.Buffers" Version="$(SystemBuffersVersion)" GeneratePathProperty="true" />
9494
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="$(SystemDiagnosticsDiagnosticSourceVersion)" GeneratePathProperty="true" />
95+
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="$(SystemSecurityCryptographyProtectedDataVersion)" GeneratePathProperty="true" />
9596
<PackageReference Include="System.Memory" Version="$(SystemMemoryVersion)" GeneratePathProperty="true" />
9697
<PackageReference Include="System.Numerics.Vectors" Version="$(SystemNumericsVectorsVersion)" GeneratePathProperty="true" />
9798
<PackageReference Include="System.Text.Encodings.Web" Version="$(SystemTextEncodingsWebVersion)" GeneratePathProperty="true" />
@@ -510,6 +511,13 @@
510511
<Link>Microsoft.Identity.Client.Extensions.Msal.dll</Link>
511512
<Visible>False</Visible>
512513
</EmbeddedResource>
514+
<EmbeddedResource Include="$(PkgMicrosoft_IdentityModel_Abstractions)\lib\net461\Microsoft.IdentityModel.Abstractions.dll">
515+
<Type>Non-Resx</Type>
516+
<WithCulture>false</WithCulture>
517+
<LogicalName>.\Microsoft.IdentityModel.Abstractions.dll</LogicalName>
518+
<Link>Microsoft.IdentityModel.Abstractions.dll</Link>
519+
<Visible>False</Visible>
520+
</EmbeddedResource>
513521
<EmbeddedResource Include="$(PkgMicrosoft_IdentityModel_JsonWebTokens)\lib\net461\Microsoft.IdentityModel.JsonWebTokens.dll">
514522
<Type>Non-Resx</Type>
515523
<WithCulture>false</WithCulture>
@@ -552,6 +560,13 @@
552560
<Link>System.Numerics.Vectors.dll</Link>
553561
<Visible>False</Visible>
554562
</EmbeddedResource>
563+
<EmbeddedResource Include="$(PkgSystem_Security_Cryptography_ProtectedData)\lib\net461\System.Security.Cryptography.ProtectedData.dll">
564+
<Type>Non-Resx</Type>
565+
<WithCulture>false</WithCulture>
566+
<LogicalName>.\System.Security.Cryptography.ProtectedData.dll</LogicalName>
567+
<Link>System.Security.Cryptography.ProtectedData.dll</Link>
568+
<Visible>False</Visible>
569+
</EmbeddedResource>
555570
<EmbeddedResource Include="$(PkgSystem_Text_Encodings_Web)\lib\net462\System.Text.Encodings.Web.dll">
556571
<Type>Non-Resx</Type>
557572
<WithCulture>false</WithCulture>

src/PerfView/SymbolReaderHttpHandler.cs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,26 @@ public SymbolReaderHttpHandler AddAzureDevOpsAuthentication(TextWriter log, bool
162162
return AddHandler(new AzureDevOpsHandler(log, new DefaultAzureCredential(options)));
163163
}
164164

165+
/// <summary>
166+
/// Add a handler for Symweb authentication using local credentials.
167+
/// It will try to use cached credentials from Visual Studio, VS Code,
168+
/// Azure Powershell and Azure CLI.
169+
/// </summary>
170+
/// <param name="log">A logger.</param>
171+
/// <param name="silent">If no local credentials can be found, then a browser window will
172+
/// be opened to prompt the user. Set this to true to if you don't want that.</param>
173+
/// <returns>This instance for fluent chaining.</returns>
174+
public SymbolReaderHttpHandler AddSymwebAuthentication(TextWriter log, bool silent = false)
175+
{
176+
DefaultAzureCredentialOptions options = new DefaultAzureCredentialOptions
177+
{
178+
ExcludeInteractiveBrowserCredential = silent,
179+
ExcludeManagedIdentityCredential = true // This is not designed to be used in a service.
180+
};
181+
182+
return AddHandler(new SymwebHandler(log, new DefaultAzureCredential(options)));
183+
}
184+
165185
/// <summary>
166186
/// Add a handler for GitHub device flow authentication.
167187
/// </summary>
@@ -1572,6 +1592,101 @@ private static bool TryGetNextChallengeParameter(ref ReadOnlySpan<char> paramete
15721592
}
15731593
}
15741594

1595+
/// <summary>
1596+
/// A handler that adds authorization for Symweb.
1597+
/// </summary>
1598+
internal sealed class SymwebHandler : SymbolReaderAuthHandlerBase
1599+
{
1600+
/// <summary>
1601+
/// The value of <see cref="Symweb.Scope"/> stored in a single element
1602+
/// array suitable for passing to
1603+
/// <see cref="TokenCredential.GetTokenAsync(TokenRequestContext, CancellationToken)"/>.
1604+
/// </summary>
1605+
private static readonly string[] s_scopes = new[] { Symweb.Scope };
1606+
1607+
/// <summary>
1608+
/// Prefix to put in front of logging messages.
1609+
/// </summary>
1610+
private const string LogPrefix = "SymwebAuth: ";
1611+
1612+
/// <summary>
1613+
/// A provider of access tokens.
1614+
/// </summary>
1615+
private readonly TokenCredential _tokenCredential;
1616+
1617+
/// <summary>
1618+
/// Protect <see cref="_tokenCredential"/> against concurrent access.
1619+
/// </summary>
1620+
private readonly SemaphoreSlim _tokenCredentialGate = new SemaphoreSlim(initialCount: 1);
1621+
1622+
/// <summary>
1623+
/// An HTTP client used to discover the authority (login endpoint and tenant) for Symweb.
1624+
/// </summary>
1625+
private readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler() { CheckCertificateRevocationList = true });
1626+
1627+
/// <summary>
1628+
/// Construct a new <see cref="SymwebHandler"/> instance.
1629+
/// </summary>
1630+
/// <param name="tokenCredential">A provider of access tokens.</param>
1631+
public SymwebHandler(TextWriter log, TokenCredential tokenCredential) : base(log, LogPrefix)
1632+
{
1633+
_tokenCredential = tokenCredential ?? throw new ArgumentNullException(nameof(tokenCredential));
1634+
}
1635+
1636+
/// <summary>
1637+
/// Try to find the authority endpoint for Symweb
1638+
/// given a full URI.
1639+
/// </summary>
1640+
/// <param name="requestUri">The request URI.</param>
1641+
/// <param name="authority">The authority, if found.</param>
1642+
/// <returns>True if <paramref name="requestUri"/> represents a path to a
1643+
/// resource in Symweb.</returns>
1644+
protected override bool TryGetAuthority(Uri requestUri, out Uri authority) => Symweb.TryGetAuthority(requestUri, out authority);
1645+
1646+
/// <summary>
1647+
/// Get a token to access Symweb.
1648+
/// </summary>
1649+
/// <param name="context">The request context.</param>
1650+
/// <param name="next">The next handler.</param>
1651+
/// <param name="authority">The Symweb instance.</param>
1652+
/// <param name="cancellationToken">A cancellation token.</param>
1653+
/// <returns>An access token, or null if one could not be obtained.</returns>
1654+
protected override async Task<AuthToken?> GetAuthTokenAsync(RequestContext context, SymbolReaderHandlerDelegate next, Uri authority, CancellationToken cancellationToken)
1655+
{
1656+
// Get a new access token from the credential provider.
1657+
WriteLog("Asking for authorization to access {0}", authority);
1658+
AuthToken token = await GetTokenAsync(cancellationToken).ConfigureAwait(false);
1659+
1660+
return token;
1661+
}
1662+
1663+
/// <summary>
1664+
/// Get a new access token for Symweb from the <see cref="TokenCredential"/>.
1665+
/// </summary>
1666+
/// <param name="cancellationToken">A cancellation token.</param>
1667+
/// <returns>The access token.</returns>
1668+
private async Task<AuthToken> GetTokenAsync(CancellationToken cancellationToken)
1669+
{
1670+
await _tokenCredentialGate.WaitAsync(cancellationToken).ConfigureAwait(false);
1671+
try
1672+
{
1673+
// Use the token credential provider to acquire a new token.
1674+
TokenRequestContext requestContext = new TokenRequestContext(s_scopes);
1675+
AccessToken accessToken = await _tokenCredential.GetTokenAsync(requestContext, cancellationToken).ConfigureAwait(false);
1676+
return AuthToken.FromAzureCoreAccessToken(accessToken);
1677+
}
1678+
catch (Exception ex)
1679+
{
1680+
WriteStatusLog("Exception getting token. {0}", ex);
1681+
throw;
1682+
}
1683+
finally
1684+
{
1685+
_tokenCredentialGate.Release();
1686+
}
1687+
}
1688+
}
1689+
15751690
/// <summary>
15761691
/// A handler that handles GitHub device flow authorization.
15771692
/// </summary>
@@ -1861,6 +1976,64 @@ private sealed class AccessTokenResponse
18611976
}
18621977
}
18631978

1979+
/// <summary>
1980+
/// Contains constants, static properties and helper methods pertinent to Symweb.
1981+
/// </summary>
1982+
internal static class Symweb
1983+
{
1984+
/// <summary>
1985+
/// The OAuth scope to use when requesting tokens for Symweb.
1986+
/// </summary>
1987+
public const string Scope = "af9e1c69-e5e9-4331-8cc5-cdf93d57bafa/.default";
1988+
1989+
/// <summary>
1990+
/// The url host for Symweb.
1991+
/// </summary>
1992+
public const string SymwebHost = "symweb.azurefd.net";
1993+
1994+
/// <summary>
1995+
/// Try to find the authority endpoint for Symweb given a full URI.
1996+
/// </summary>
1997+
/// <param name="requestUri">The request URI.</param>
1998+
/// <param name="authority">The authority, if found.</param>
1999+
/// <returns>True if <paramref name="requestUri"/> represents a path to a
2000+
/// resource in Symweb.</returns>
2001+
public static bool TryGetAuthority(Uri requestUri, out Uri authority)
2002+
{
2003+
if (!requestUri.IsAbsoluteUri)
2004+
{
2005+
authority = null;
2006+
return false;
2007+
}
2008+
2009+
UriBuilder builder = null;
2010+
string host = requestUri.DnsSafeHost;
2011+
if (host.Equals(SymwebHost, StringComparison.OrdinalIgnoreCase))
2012+
{
2013+
builder = new UriBuilder
2014+
{
2015+
Host = SymwebHost
2016+
};
2017+
}
2018+
2019+
if (builder is null)
2020+
{
2021+
// Not a Symweb URI.
2022+
authority = null;
2023+
return false;
2024+
}
2025+
2026+
builder.Scheme = requestUri.Scheme;
2027+
if (!requestUri.IsDefaultPort)
2028+
{
2029+
builder.Port = requestUri.Port;
2030+
}
2031+
2032+
authority = builder.Uri;
2033+
return true;
2034+
}
2035+
}
2036+
18642037
/// <summary>
18652038
/// Contains constants, static properties and helper methods pertinent to Azure DevOps.
18662039
/// </summary>

src/TraceEvent/Symbols/SymbolPath.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,7 @@ public static string MicrosoftSymbolServerPath
7070
if (s_MicrosoftSymbolServerPath == null)
7171
{
7272
s_MicrosoftSymbolServerPath = s_MicrosoftSymbolServerPath +
73-
";" + @"SRV*https://msdl.microsoft.com/download/symbols" + // Operatig system Symbols
74-
";" + @"SRV*https://nuget.smbsrc.net" + // Nuget symbols
75-
";" + @"SRV*https://referencesource.microsoft.com/symbols"; // .NET Runtime desktop symbols
73+
";" + @"SRV*https://msdl.microsoft.com/download/symbols";
7674
}
7775
return s_MicrosoftSymbolServerPath;
7876
}

src/TraceEvent/Symbols/SymbolReader.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,7 +1072,7 @@ internal bool GetPhysicalFileFromServer(string serverPath, string pdbIndexPath,
10721072
{
10731073
m_log.WriteLine("FindSymbolFilePath: In task, sending HTTP request {0}", fullUri);
10741074

1075-
var responseTask = HttpClient.GetAsync(fullUri);
1075+
var responseTask = HttpClient.GetAsync(fullUri, HttpCompletionOption.ResponseHeadersRead);
10761076
responseTask.Wait();
10771077
var response = responseTask.Result.EnsureSuccessStatusCode();
10781078

@@ -1140,8 +1140,8 @@ internal bool GetPhysicalFileFromServer(string serverPath, string pdbIndexPath,
11401140
}
11411141
});
11421142

1143-
// Wait 25 seconds allowing for interruptions.
1144-
var limit = 250;
1143+
// Wait 60 seconds allowing for interruptions.
1144+
var limit = 600;
11451145

11461146
for (int i = 0; i < limit; i++)
11471147
{

0 commit comments

Comments
 (0)