Skip to content

Commit ab2caef

Browse files
committed
azrepos: look at wwwauth[] for authority in first instance
Use the new `wwwauth[]` header information from Git to determine the Azure authority for Azure Repos, before needing to resort to a cached value, or making a HEAD call.
1 parent 1b13045 commit ab2caef

File tree

2 files changed

+86
-10
lines changed

2 files changed

+86
-10
lines changed

src/shared/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,42 @@ public async Task AzureReposHostProvider_UnconfigureAsync_User_Windows_UseHttpPa
643643
Assert.False(context.Git.Configuration.Global.TryGetValue(AzDevUseHttpPathKey, out _));
644644
}
645645

646+
[Theory]
647+
[InlineData(false, null, "")]
648+
[InlineData(false, null, " ")]
649+
[InlineData(false, null, null)]
650+
[InlineData(false, null, "Basic realm=\"test\"")]
651+
[InlineData(false, null, "Basic realm=\"https://tfsprodwcus0.app.visualstudio.com/\"")]
652+
[InlineData(false, null, "TFS-Federated")]
653+
[InlineData(true, "https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3",
654+
"Bearer authorization_uri=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3")]
655+
[InlineData(true, "https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3",
656+
"bEArEr auThORizAtIoN_uRi=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3")]
657+
[InlineData(true, "https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3",
658+
"\"Bearer authorization_uri=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3\"")]
659+
[InlineData(true, "https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3",
660+
"'Bearer authorization_uri=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3'")]
661+
[InlineData(true, "https://login.microsoftonline.com/tenant1",
662+
"Bearer authorization_uri=https://login.microsoftonline.com/tenant1",
663+
"Bearer authorization_uri=https://login.microsoftonline.com/tenant2",
664+
"Bearer authorization_uri=https://login.microsoftonline.com/tenant3")]
665+
[InlineData(true, "https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3",
666+
"Bearer authorization_uri=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3",
667+
"Basic realm=\"https://tfsprodwcus0.app.visualstudio.com/\"",
668+
"TFS-Federated")]
669+
[InlineData(true, "https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3",
670+
"TFS-Federated",
671+
"Basic realm=\"https://tfsprodwcus0.app.visualstudio.com/\"",
672+
"Bearer authorization_uri=https://login.microsoftonline.com/79c4d065-d599-442e-b0ea-c4ab36ad63c3")]
673+
public void AzureReposHostProvider_TryGetAuthorityFromHeaders(
674+
bool expectedResult, string expectedAuthority, params string[] headers)
675+
{
676+
bool actualResult = AzureReposHostProvider.TryGetAuthorityFromHeaders(headers, out string actualAuthority);
677+
678+
Assert.Equal(expectedResult, actualResult);
679+
Assert.Equal(expectedAuthority, actualAuthority);
680+
}
681+
646682
private static IMicrosoftAuthenticationResult CreateAuthResult(string upn, string token)
647683
{
648684
return new MockMsAuthResult

src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.CommandLine;
44
using System.Linq;
55
using System.Net.Http;
6+
using System.Text.RegularExpressions;
67
using System.Threading.Tasks;
78
using GitCredentialManager;
89
using GitCredentialManager.Authentication;
@@ -74,10 +75,9 @@ public bool IsSupported(HttpResponseMessage response)
7475

7576
public async Task<ICredential> GetCredentialAsync(InputArguments input)
7677
{
77-
Uri remoteUri = input.GetRemoteUri();
78-
7978
if (UsePersonalAccessTokens())
8079
{
80+
Uri remoteUri = input.GetRemoteUri();
8181
string service = GetServiceName(remoteUri);
8282
string account = GetAccountNameForCredentialQuery(input);
8383

@@ -104,7 +104,7 @@ public async Task<ICredential> GetCredentialAsync(InputArguments input)
104104
{
105105
// Include the username request here so that we may use it as an override
106106
// for user account lookups when getting Azure Access Tokens.
107-
var azureResult = await GetAzureAccessTokenAsync(remoteUri, input.UserName);
107+
var azureResult = await GetAzureAccessTokenAsync(input);
108108
return new GitCredential(azureResult.AccountUpn, azureResult.AccessToken);
109109
}
110110
}
@@ -222,8 +222,11 @@ private async Task<ICredential> GeneratePersonalAccessTokenAsync(InputArguments
222222
return new GitCredential(result.AccountUpn, pat);
223223
}
224224

225-
private async Task<IMicrosoftAuthenticationResult> GetAzureAccessTokenAsync(Uri remoteUri, string userName)
225+
private async Task<IMicrosoftAuthenticationResult> GetAzureAccessTokenAsync(InputArguments input)
226226
{
227+
Uri remoteUri = input.GetRemoteUri();
228+
string userName = input.UserName;
229+
227230
// We should not allow unencrypted communication and should inform the user
228231
if (StringComparer.OrdinalIgnoreCase.Equals(remoteUri.Scheme, "http"))
229232
{
@@ -234,14 +237,27 @@ private async Task<IMicrosoftAuthenticationResult> GetAzureAccessTokenAsync(Uri
234237
Uri orgUri = UriHelpers.CreateOrganizationUri(remoteUri, out string orgName);
235238

236239
_context.Trace.WriteLine($"Determining Microsoft Authentication authority for Azure DevOps organization '{orgName}'...");
237-
string authAuthority = _authorityCache.GetAuthority(orgName);
238-
if (authAuthority is null)
240+
if (TryGetAuthorityFromHeaders(input.WwwAuth, out string authAuthority))
241+
{
242+
_context.Trace.WriteLine("Authority was found in WWW-Authenticate headers from Git input.");
243+
}
244+
else
239245
{
240-
// If there is no cached value we must query for it and cache it for future use
241-
_context.Trace.WriteLine($"No cached authority value - querying {orgUri} for authority...");
242-
authAuthority = await _azDevOps.GetAuthorityAsync(orgUri);
243-
_authorityCache.UpdateAuthority(orgName, authAuthority);
246+
// Try to get the authority from the cache
247+
authAuthority = _authorityCache.GetAuthority(orgName);
248+
if (authAuthority is null)
249+
{
250+
// If there is no cached value we must query for it and cache it for future use
251+
_context.Trace.WriteLine($"No cached authority value - querying {orgUri} for authority...");
252+
authAuthority = await _azDevOps.GetAuthorityAsync(orgUri);
253+
_authorityCache.UpdateAuthority(orgName, authAuthority);
254+
}
255+
else
256+
{
257+
_context.Trace.WriteLine("Authority was found in cache.");
258+
}
244259
}
260+
245261
_context.Trace.WriteLine($"Authority is '{authAuthority}'.");
246262

247263
//
@@ -284,6 +300,30 @@ private async Task<IMicrosoftAuthenticationResult> GetAzureAccessTokenAsync(Uri
284300
return result;
285301
}
286302

303+
internal /* for testing purposes */ static bool TryGetAuthorityFromHeaders(IEnumerable<string> headers, out string authority)
304+
{
305+
authority = null;
306+
307+
if (headers is null)
308+
{
309+
return false;
310+
}
311+
312+
var regex = new Regex(@"authorization_uri=""?(?<authority>.+)""?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
313+
314+
foreach (string header in headers)
315+
{
316+
Match match = regex.Match(header);
317+
if (match.Success)
318+
{
319+
authority = match.Groups["authority"].Value.Trim(new[] { '"', '\'' });
320+
return true;
321+
}
322+
}
323+
324+
return false;
325+
}
326+
287327
private string GetClientId()
288328
{
289329
// Check for developer override value

0 commit comments

Comments
 (0)