Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ private AuthenticationResult(
}

UniqueId = msalIdTokenCacheItem?.IdToken?.GetUniqueId();
TenantId = msalIdTokenCacheItem?.IdToken?.TenantId;
// For client credentials flow, ID token is not available, so fall back to access token's tenant
// See https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/5743
TenantId = msalIdTokenCacheItem?.IdToken?.TenantId ?? msalAccessTokenCacheItem?.TenantId;
IdToken = msalIdTokenCacheItem?.Secret;
SpaAuthCode = spaAuthCode;
_authenticationScheme = authenticationScheme;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,130 @@ public async Task WithOnBeforeTokenRequest_TestAsync(Cloud cloud, TargetFramewor
Assert.AreEqual(TokenSource.Cache, authResult.AuthenticationResultMetadata.TokenSource);
}

[TestMethod]
[DataRow(TargetFrameworks.NetCore)]
[Description("Issue #5743: Validates that AuthenticationResult.TenantId is not null for client credentials")]
public async Task ClientCredentials_TenantId_ShouldNotBeNull_Issue5743_Async(TargetFrameworks runOn)
{
runOn.AssertFramework();

var appConfig = await LabResponseHelper.GetAppConfigAsync(KeyVaultSecrets.AppS2S).ConfigureAwait(false);
var cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName);
string[] scopes = new[] { "https://vault.azure.net/.default" };

var confidentialApp = ConfidentialClientApplicationBuilder
.Create(appConfig.AppId)
.WithAuthority(appConfig.Authority, true)
.WithCertificate(cert)
.WithTestLogging()
.Build();

// First call - from identity provider
var result = await confidentialApp
.AcquireTokenForClient(scopes)
.ExecuteAsync()
.ConfigureAwait(false);

Assert.IsNotNull(result.TenantId,
"TenantId should not be null in AuthenticationResult for client credentials flow!");
Assert.AreEqual(appConfig.TenantId, result.TenantId,
"TenantId should match the configured tenant");
Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);

// Second call - from cache
result = await confidentialApp
.AcquireTokenForClient(scopes)
.ExecuteAsync()
.ConfigureAwait(false);

Assert.IsNotNull(result.TenantId,
"TenantId should not be null even when token comes from cache!");
Assert.AreEqual(appConfig.TenantId, result.TenantId,
"Cached token should preserve TenantId");
Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource);
}

[TestMethod]
[DataRow(TargetFrameworks.NetCore)]
[Description("Issue #5743: Validates TenantId when using WithTenantId override with organizations authority")]
public async Task ClientCredentials_WithTenantIdOverride_ShouldPopulateTenantId_Issue5743_Async(TargetFrameworks runOn)
{
runOn.AssertFramework();

var appConfig = await LabResponseHelper.GetAppConfigAsync(KeyVaultSecrets.AppS2S).ConfigureAwait(false);
var cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName);
string[] scopes = new[] { "https://vault.azure.net/.default" };

// Build app with "organizations" authority
var confidentialApp = ConfidentialClientApplicationBuilder
.Create(appConfig.AppId)
.WithAuthority("https://login.microsoftonline.com/organizations", true)
.WithCertificate(cert)
.WithTestLogging()
.Build();

// Request token with specific tenant override
var result = await confidentialApp
.AcquireTokenForClient(scopes)
.WithTenantId(appConfig.TenantId)
.ExecuteAsync()
.ConfigureAwait(false);

// ❌ BUG #5743: This assertion will FAIL
Assert.IsNotNull(result.TenantId,
"❌ BUG #5743: TenantId must be populated when using WithTenantId override!");
Assert.AreEqual(appConfig.TenantId, result.TenantId,
"TenantId should match the overridden tenant, not 'organizations'");
Assert.AreEqual(TokenSource.IdentityProvider, result.AuthenticationResultMetadata.TokenSource);

// Second call from cache
result = await confidentialApp
.AcquireTokenForClient(scopes)
.WithTenantId(appConfig.TenantId)
.ExecuteAsync()
.ConfigureAwait(false);

Assert.IsNotNull(result.TenantId,
"❌ BUG #5743: Cached token should also have TenantId populated!");
Assert.AreEqual(appConfig.TenantId, result.TenantId);
Assert.AreEqual(TokenSource.Cache, result.AuthenticationResultMetadata.TokenSource);
}

[TestMethod]
[DataRow(TargetFrameworks.NetCore)]
[Description("Issue #5743: Rapid tenant switching should maintain correct TenantId")]
public async Task ClientCredentials_RapidTenantSwitching_Issue5743_Async(TargetFrameworks runOn)
{
runOn.AssertFramework();

var appConfig = await LabResponseHelper.GetAppConfigAsync(KeyVaultSecrets.AppS2S).ConfigureAwait(false);
var cert = CertificateHelper.FindCertificateByName(TestConstants.AutomationTestCertName);
string[] scopes = new[] { "https://vault.azure.net/.default" };

var confidentialApp = ConfidentialClientApplicationBuilder
.Create(appConfig.AppId)
.WithAuthority("https://login.microsoftonline.com/organizations", true)
.WithCertificate(cert)
.WithTestLogging()
.Build();

// Rapidly switch between same tenant multiple times with force refresh
for (int i = 0; i < 10; i++)
{
var result = await confidentialApp
.AcquireTokenForClient(scopes)
.WithTenantId(appConfig.TenantId)
.WithForceRefresh(true)
.ExecuteAsync()
.ConfigureAwait(false);

Assert.IsNotNull(result.TenantId,
$"❌ BUG #5743: Iteration {i}: TenantId should not be null!");
Assert.AreEqual(appConfig.TenantId, result.TenantId,
$"Iteration {i}: TenantId should match requested tenant");
}
}

[RunOn(TargetFrameworks.NetCore)]
public async Task ByRefreshTokenTestAsync()
{
Expand Down Expand Up @@ -357,6 +481,12 @@ private async Task RunClientCredsAsync(Cloud cloud, CredentialType credentialTyp
MsalAssert.AssertAuthResult(authResult);
appCacheRecorder.AssertAccessCounts(1, 1);
Assert.AreEqual(TokenSource.IdentityProvider, authResult.AuthenticationResultMetadata.TokenSource);

// Issue #5743: Validate TenantId is populated
Assert.IsNotNull(authResult.TenantId,
"❌ BUG #5743: TenantId should not be null for client credentials!");
Assert.AreEqual(tenantId, authResult.TenantId,
"TenantId should match the tenant from authority");
Assert.IsTrue(appCacheRecorder.LastAfterAccessNotificationArgs.IsApplicationCache);
Assert.IsTrue(appCacheRecorder.LastAfterAccessNotificationArgs.HasTokens);
Assert.AreEqual(correlationId, appCacheRecorder.LastAfterAccessNotificationArgs.CorrelationId);
Expand Down Expand Up @@ -385,6 +515,12 @@ private async Task RunClientCredsAsync(Cloud cloud, CredentialType credentialTyp

appCacheRecorder.AssertAccessCounts(2, 1);
Assert.AreEqual(TokenSource.Cache, authResult.AuthenticationResultMetadata.TokenSource);

// Issue #5743: Validate TenantId is preserved in cached result
Assert.IsNotNull(authResult.TenantId,
"❌ BUG #5743: TenantId should not be null even from cache!");
Assert.AreEqual(tenantId, authResult.TenantId,
"Cached token should preserve TenantId");
Assert.IsTrue(appCacheRecorder.LastAfterAccessNotificationArgs.IsApplicationCache);
Assert.IsTrue(appCacheRecorder.LastAfterAccessNotificationArgs.HasTokens);
Assert.AreNotEqual(correlationId, appCacheRecorder.LastAfterAccessNotificationArgs.CorrelationId);
Expand Down
Loading