@@ -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>
0 commit comments