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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@ objd/

key*.xml

# MSBuild temp files (.NET 9+ SDK writes these to a Temp/ folder in the repo root)
Temp/

# bin files

*.bin
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ internal static class CertificateErrorMessage
"For instance, in the appsettings.json file. ";
public const string BothClientSecretAndCertificateProvided =
"IDW10105: Both client secret and client certificate, cannot be included in the configuration of the web app when calling a web API. ";
public const string ClientCertificatesHaveExpiredOrCannotBeLoaded = "IDW10109: All client certificates passed to the configuration have expired or can't be loaded. ";
public const string CustomProviderNameAlreadyExists =
"IDW10111 The custom signed assertion provider '{0}' already exists, only the the first instance of ICustomSignedAssertionProvider with this name will be used.";
public const string CustomProviderNameNullOrEmpty = "IDW10112: You configured a custom signed assertion but did not specify a provider name in the CustomSignedAssertionProviderName property of the CredentialDescription. Please specify the name of the custom assertion provider.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public CredentialsProvider(
IEnumerable<CredentialDescription> clientCredentials = options.ClientCredentials ?? [];

string errorMessage = "\n";
List<Exception>? exceptions = null;

foreach (CredentialDescription credential in clientCredentials)
{
Expand All @@ -72,6 +73,7 @@ public CredentialsProvider(
{
LogMessages.AttemptToLoadCredentialsFailed(_logger, credential, ex);
errorMessage += $"Credential {credential.Id} failed because: {ex} \n";
(exceptions ??= []).Add(ex);
}

if (credential.CredentialType == CredentialType.SignedAssertion)
Expand Down Expand Up @@ -139,9 +141,19 @@ public CredentialsProvider(

if (clientCredentials.Any(c => c.CredentialType == CredentialType.Certificate || c.CredentialType == CredentialType.SignedAssertion))
{
// Preserve the original exceptions so callers can inspect service-level
// error details such as AADSTS error codes via InnerException.
Exception? innerException = exceptions switch
{
{ Count: 1 } => exceptions[0],
{ Count: > 1 } => new AggregateException(exceptions),
_ => null,
};

throw new ArgumentException(
IDWebErrorMessage.ClientCertificatesHaveExpiredOrCannotBeLoaded + errorMessage,
nameof(clientCredentials));
nameof(clientCredentials),
innerException);
}

_logger.LogInformation($"No client credential could be used. Secret may have been defined elsewhere. " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal static class IDWebErrorMessage
public const string ConfigurationOptionRequired = "IDW10106: The '{0}' option must be provided. ";
public const string ScopesNotConfiguredInConfigurationOrViaDelegate = "IDW10107: Scopes need to be passed-in either by configuration or by the delegate overriding it. ";
public const string MissingRequiredScopesForAuthorizationFilter = "IDW10108: RequiredScope Attribute does not contain a value. The scopes need to be set on the controller, the page or action. See https://aka.ms/ms-id-web/required-scope-attribute. ";
public const string ClientCertificatesHaveExpiredOrCannotBeLoaded = "IDW10109: No credential could be loaded. This can happen when certificates passed to the configuration have expired or can't be loaded and the code isn't running on Azure to be able to use Managed Identity, Pod Identity etc. Details: ";
public const string ClientCertificatesHaveExpiredOrCannotBeLoaded = "IDW10109: No credential could be loaded. This can happen when certificates have expired or can't be loaded, when a signed assertion (e.g., Federated Identity Credential) fails to acquire an exchange token, or when the code isn't running on Azure to use Managed Identity. Check the inner exception for service-level error details (e.g., AADSTS error codes). Details: ";
Comment thread
Avery-Dunn marked this conversation as resolved.
public const string ClientSecretAndCredentialsCannotBeCombined = "IDW10110: ClientSecret top level configuration cannot be combined with ClientCredentials. Instead, add a new entry in the ClientCredentials array describing the secret.";
public const string MissingTokenBindingCertificate = "IDW10115: Token binding requires either a signing certificate or a binding-aware signed assertion (e.g., from a managed identity supporting mTLS PoP). The loaded credential provides neither.";
public const string TokenBindingRequiresEnabledAppTokenAcquisition = "IDW10116: Token binding requires enabled app token acquisition.";
Expand Down
27 changes: 8 additions & 19 deletions src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1535,25 +1535,14 @@ private async Task<IConfidentialClientApplication> BuildConfidentialClientApplic
}
else
{
try
{
await builder.WithClientCredentialsAsync(
mergedOptions,
_credentialsProvider,
new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority)
{
Protocol = isTokenBinding ? ProtocolNames.MtlsPop : ProtocolNames.Bearer,
},
isTokenBinding);
}
catch (ArgumentException ex) when (ex.Message == IDWebErrorMessage.ClientCertificatesHaveExpiredOrCannotBeLoaded)
{
Logger.TokenAcquisitionError(
_logger,
IDWebErrorMessage.ClientCertificatesHaveExpiredOrCannotBeLoaded,
ex);
throw;
}
await builder.WithClientCredentialsAsync(
mergedOptions,
_credentialsProvider,
new CredentialSourceLoaderParameters(mergedOptions.ClientId!, authority)
{
Protocol = isTokenBinding ? ProtocolNames.MtlsPop : ProtocolNames.Bearer,
},
isTokenBinding);
}

IConfidentialClientApplication app = builder.Build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -653,5 +653,98 @@ public async Task WithClientCredentialsAsync_CertificateWithUseBoundCredentialFa

#endregion

#region IDW10109 inner exception preservation tests

[Fact]
public async Task AllCredentialsFail_SingleFailure_PreservesInnerExceptionAndErrorText()
{
// Arrange
var logger = Substitute.For<ILogger<CredentialsProvider>>();
ICredentialsLoader credLoader = Substitute.For<ICredentialsLoader>();

var msalException = new MsalServiceException(
"AADSTS700027",
"AADSTS700027: Client assertion contains an invalid signature.");

credLoader.LoadCredentialsIfNeededAsync(Arg.Any<CredentialDescription>(), Arg.Any<CredentialSourceLoaderParameters>())
.Returns(args =>
{
var cd = (args[0] as CredentialDescription)!;
cd.Skip = true;
return Task.FromException(msalException);
});

CredentialsProvider provider = new CredentialsProvider(logger, credLoader, [], null);

var credential = new CredentialDescription
{
SourceType = CredentialSource.SignedAssertionFromManagedIdentity,
ManagedIdentityClientId = "test-client-id"
};

// Act
var ex = await Assert.ThrowsAsync<ArgumentException>(() => provider.GetCredentialAsync(
new MergedOptions { ClientCredentials = new[] { credential } }, null));

// Assert — original exception is preserved as InnerException
Assert.NotNull(ex.InnerException);
var inner = Assert.IsType<MsalServiceException>(ex.InnerException);
Assert.Equal("AADSTS700027", inner.ErrorCode);

// Assert — error message includes IDW10109 code and FIC guidance
Assert.StartsWith("IDW10109:", ex.Message, StringComparison.Ordinal);
Assert.Contains("Federated Identity Credential", ex.Message, StringComparison.Ordinal);
Assert.Contains("inner exception", ex.Message, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public async Task AllCredentialsFail_MultipleFailures_PreservesAllExceptionsViaAggregateException()
{
// Arrange
var logger = Substitute.For<ILogger<CredentialsProvider>>();
ICredentialsLoader credLoader = Substitute.For<ICredentialsLoader>();

var msalException = new MsalServiceException("AADSTS700027", "Invalid signature.");
var certException = new InvalidOperationException("Certificate not found in store.");

int callCount = 0;
credLoader.LoadCredentialsIfNeededAsync(Arg.Any<CredentialDescription>(), Arg.Any<CredentialSourceLoaderParameters>())
.Returns(args =>
{
var cd = (args[0] as CredentialDescription)!;
cd.Skip = true;
return Task.FromException(callCount++ == 0 ? (Exception)msalException : certException);
});

CredentialsProvider provider = new CredentialsProvider(logger, credLoader, [], null);

var credentials = new[]
{
new CredentialDescription
{
SourceType = CredentialSource.SignedAssertionFromManagedIdentity,
ManagedIdentityClientId = "test-client-id"
},
new CredentialDescription
{
SourceType = CredentialSource.StoreWithDistinguishedName,
CertificateStorePath = "LocalMachine/My",
CertificateDistinguishedName = "CN=Test"
}
};

// Act
var ex = await Assert.ThrowsAsync<ArgumentException>(() => provider.GetCredentialAsync(
new MergedOptions { ClientCredentials = credentials }, null));

// Assert — InnerException is AggregateException containing both originals
Assert.NotNull(ex.InnerException);
var aggEx = Assert.IsType<AggregateException>(ex.InnerException);
Assert.Equal(2, aggEx.InnerExceptions.Count);
Assert.IsType<MsalServiceException>(aggEx.InnerExceptions[0]);
Assert.IsType<InvalidOperationException>(aggEx.InnerExceptions[1]);
}

#endregion
}
}
Loading