Skip to content

Commit f80007b

Browse files
committed
azrepos: use organizations authority for MSA accounts
Use the /organizations authority for MSA accounts with Azure DevOps/Repos. This is because we're using MSA pass-through, an internal Microsoft mechanism to support both MSA and 'work' (AAD) accounts with the same auth stacks. You should be able to use /common, but this doesn't work. At the same time we're using ADAL Obj-C on macOS rather than MSAL.NET like we do on Windows, and ADAL speaks to the "v1" AAD endpoints, which don't know the /organizations tenant :( For macOS we need to fudge the authority _back_ to /common for MSA accounts.
1 parent 5c23c97 commit f80007b

File tree

3 files changed

+30
-4
lines changed

3 files changed

+30
-4
lines changed

src/osx/Microsoft.Authentication.Helper.Mac/Source/main.m

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,23 @@ int main(int argc, const char * argv[]) {
100100
NSString* redirectUri = [configs objectForKey:@"redirectUri"];
101101
NSString* interactive = [configs objectForKey:@"interactive"];
102102

103+
// Because ADAL only supports the v1 endpoints we need to transform any request
104+
// for the /organizations or /consumers authority to the /common one or else
105+
// we get errors back from the server.
106+
NSString *lowerAuthority = [authority lowercaseString];
107+
if ([lowerAuthority hasSuffix:@"/organizations"] || [lowerAuthority hasSuffix:@"/consumers"])
108+
{
109+
NSError *error = nil;
110+
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"/(organizations|consumers)$"
111+
options:NSRegularExpressionCaseInsensitive
112+
error:&error];
113+
NSString* newAuthority = [regex stringByReplacingMatchesInString:authority
114+
options:0
115+
range:NSMakeRange(0, authority.length)
116+
withTemplate:@"/common"];
117+
authority = newAuthority;
118+
}
119+
103120
// We only perform interactive flows
104121
if (isTruthy(interactive))
105122
{

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class AzureDevOpsApiTests
2121
private const string ExpectedLocationServicePath = "_apis/ServiceDefinitions/LocationService2/951917AC-A960-4999-8464-E3F0AA25B381?api-version=1.0";
2222
private const string ExpectedIdentityServicePath = "_apis/token/sessiontokens?api-version=1.0&tokentype=compact";
2323
private const string CommonAuthority = "https://login.microsoftonline.com/common";
24+
private const string OrganizationsAuthority = "https://login.microsoftonline.com/organizations";
2425

2526
[Fact]
2627
public async Task AzureDevOpsRestApi_GetAuthorityAsync_NullUri_ThrowsException()
@@ -169,13 +170,15 @@ public async Task AzureDevOpsRestApi_GetAuthorityAsync_VssResourceTenantMultiple
169170
}
170171

171172
[Fact]
172-
public async Task AzureDevOpsRestApi_GetAuthorityAsync_VssResourceTenantMsa_ReturnsCommonAuthority()
173+
public async Task AzureDevOpsRestApi_GetAuthorityAsync_VssResourceTenantMsa_ReturnsOrganizationsAuthority()
173174
{
174175
var context = new TestCommandContext();
175176
var uri = new Uri("https://example.com");
176177
var msaTenantId = Guid.Empty;
177178

178-
const string expectedAuthority = CommonAuthority;
179+
// This is only the case because we're using MSA pass-through.. in the future, if and when we
180+
// move away from MSA pass-through, this should be the common authority.
181+
const string expectedAuthority = OrganizationsAuthority;
179182

180183
var httpResponse = new HttpResponseMessage(HttpStatusCode.Unauthorized)
181184
{

src/shared/Microsoft.AzureRepos/AzureDevOpsRestApi.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ public async Task<string> GetAuthorityAsync(Uri organizationUri)
3737
const string authorityBase = "https://login.microsoftonline.com/";
3838
const string commonAuthority = authorityBase + "common";
3939

40+
// We should be using "/common" or "/consumer" as the authority for MSA but since
41+
// Azure DevOps uses MSA pass-through (an internal hack to support MSA and AAD
42+
// accounts in the same auth stack), which actually need to consult the "/organizations"
43+
// authority instead.
44+
const string msaAuthority = authorityBase + "organizations";
45+
4046
_context.Trace.WriteLine($"HTTP: HEAD {organizationUri}");
4147
using (HttpResponseMessage response = await HttpClient.HeadAsync(organizationUri))
4248
{
@@ -74,14 +80,14 @@ public async Task<string> GetAuthorityAsync(Uri organizationUri)
7480
if (tenantIds.Length == 1 && Guid.TryParse(tenantIds[0], out guid) && guid == Guid.Empty)
7581
{
7682
_context.Trace.WriteLine($"Found {AzureDevOpsConstants.VssResourceTenantHeader} header with MSA tenant ID (empty GUID).");
77-
return commonAuthority;
83+
return msaAuthority;
7884
}
7985
}
8086
}
8187
}
8288

8389
// Use the common authority if we can't determine a specific one
84-
_context.Trace.WriteLine("Falling back to common authority.");
90+
_context.Trace.WriteLine($"Unable to determine AAD/MSA tenant - falling back to common authority");
8591
return commonAuthority;
8692
}
8793

0 commit comments

Comments
 (0)