diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 5cd8229c5..065249331 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -44,12 +44,7 @@ jobs:
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
-
- - name: Setup .NET 6.0.301
- uses: actions/setup-dotnet@v2.1.0
- with:
- dotnet-version: 6.0.301
-
+
- name: Setup .NET 7.0.x
uses: actions/setup-dotnet@v2.1.0
with:
@@ -59,9 +54,6 @@ jobs:
- name: Setup wasm-tools
run: dotnet workload install wasm-tools
- - name: Build with .NET 6
- run: dotnet test Microsoft.Identity.Web.sln -f net6.0 -p:FROM_GITHUB_ACTION=true --configuration Release --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName!~IntegrationTests)"
-
- name: Build with .NET 7
run: dotnet test Microsoft.Identity.Web.sln -f net7.0 -p:FROM_GITHUB_ACTION=true --configuration Release --filter "(FullyQualifiedName!~Microsoft.Identity.Web.Test.Integration)&(FullyQualifiedName!~WebAppUiTests)&(FullyQualifiedName=IntegrationTests)"
diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCertificateLoader.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCertificateLoader.cs
index ad11c0abf..83845dcff 100644
--- a/src/Microsoft.Identity.Web.Certificate/DefaultCertificateLoader.cs
+++ b/src/Microsoft.Identity.Web.Certificate/DefaultCertificateLoader.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.Logging;
using Microsoft.Identity.Abstractions;
namespace Microsoft.Identity.Web
@@ -26,6 +27,20 @@ namespace Microsoft.Identity.Web
///
public class DefaultCertificateLoader : DefaultCredentialsLoader, ICertificateLoader
{
+ ///
+ /// Constructor with a logger.
+ ///
+ ///
+ public DefaultCertificateLoader(ILogger? logger) : base(logger)
+ {
+ }
+
+ ///
+ /// Default constuctor.
+ ///
+ public DefaultCertificateLoader() : this(null)
+ {
+ }
///
/// This default is overridable at the level of the credential description (for the certificate from KeyVault).
@@ -50,7 +65,7 @@ public static string? UserAssignedManagedIdentityClientId
/// First certificate in the certificate description list.
public static X509Certificate2? LoadFirstCertificate(IEnumerable certificateDescriptions)
{
- DefaultCertificateLoader defaultCertificateLoader = new();
+ DefaultCertificateLoader defaultCertificateLoader = new(null);
CertificateDescription? certDescription = certificateDescriptions.FirstOrDefault(c =>
{
defaultCertificateLoader.LoadCredentialsIfNeededAsync(c).GetAwaiter().GetResult();
@@ -67,12 +82,22 @@ public static string? UserAssignedManagedIdentityClientId
/// All the certificates in the certificate description list.
public static IEnumerable LoadAllCertificates(IEnumerable certificateDescriptions)
{
- DefaultCertificateLoader defaultCertificateLoader = new();
+ DefaultCertificateLoader defaultCertificateLoader = new(null);
+ return defaultCertificateLoader.LoadCertificates(certificateDescriptions);
+ }
+
+ ///
+ /// Load the certificates from the certificate description list.
+ ///
+ ///
+ /// a collection of certificates
+ private IEnumerable LoadCertificates(IEnumerable certificateDescriptions)
+ {
if (certificateDescriptions != null)
{
foreach (var certDescription in certificateDescriptions)
{
- defaultCertificateLoader.LoadCredentialsIfNeededAsync(certDescription).GetAwaiter().GetResult();
+ LoadCredentialsIfNeededAsync(certDescription).GetAwaiter().GetResult();
if (certDescription.Certificate != null)
{
yield return certDescription.Certificate;
diff --git a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.cs b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.cs
index af0de9211..e94b14ec4 100644
--- a/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.cs
+++ b/src/Microsoft.Identity.Web.Certificate/DefaultCredentialsLoader.cs
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
using Microsoft.Identity.Abstractions;
namespace Microsoft.Identity.Web
@@ -12,20 +14,39 @@ namespace Microsoft.Identity.Web
///
public class DefaultCredentialsLoader : ICredentialsLoader
{
+ ILogger? _logger;
+
+ ///
+ /// Constructor with a logger
+ ///
+ ///
+ public DefaultCredentialsLoader(ILogger? logger)
+ {
+ _logger = logger;
+ CredentialSourceLoaders = new Dictionary
+ {
+ { CredentialSource.KeyVault, new KeyVaultCertificateLoader() },
+ { CredentialSource.Path, new FromPathCertificateLoader() },
+ { CredentialSource.StoreWithThumbprint, new StoreWithThumbprintCertificateLoader() },
+ { CredentialSource.StoreWithDistinguishedName, new StoreWithDistinguishedNameCertificateLoader() },
+ { CredentialSource.Base64Encoded, new Base64EncodedCertificateLoader() },
+ { CredentialSource.SignedAssertionFromManagedIdentity, new SignedAssertionFromManagedIdentityCredentialLoader() },
+ { CredentialSource.SignedAssertionFilePath, new SignedAssertionFilePathCredentialsLoader(_logger) }
+ };
+ }
+
+ ///
+ /// Default constructor (for backward compatibility)
+ ///
+ public DefaultCredentialsLoader() : this(null)
+ {
+ }
+
///
/// Dictionary of credential loaders per credential source. The application can add more to
/// process additional credential sources(like dSMS).
///
- public IDictionary CredentialSourceLoaders { get; } = new Dictionary
- {
- { CredentialSource.KeyVault, new KeyVaultCertificateLoader() },
- { CredentialSource.Path, new FromPathCertificateLoader() },
- { CredentialSource.StoreWithThumbprint, new StoreWithThumbprintCertificateLoader() },
- { CredentialSource.StoreWithDistinguishedName, new StoreWithDistinguishedNameCertificateLoader() },
- { CredentialSource.Base64Encoded, new Base64EncodedCertificateLoader() },
- { CredentialSource.SignedAssertionFromManagedIdentity, new SignedAssertionFromManagedIdentityCredentialLoader() },
- { CredentialSource.SignedAssertionFilePath, new SignedAssertionFilePathCredentialsLoader() },
- };
+ public IDictionary CredentialSourceLoaders { get; }
///
/// Load the credentials from the description, if needed.
diff --git a/src/Microsoft.Identity.Web.Certificate/SignedAssertionFilePathCredentialsLoader.cs b/src/Microsoft.Identity.Web.Certificate/SignedAssertionFilePathCredentialsLoader.cs
index 6ca2bb878..bf9a40dc7 100644
--- a/src/Microsoft.Identity.Web.Certificate/SignedAssertionFilePathCredentialsLoader.cs
+++ b/src/Microsoft.Identity.Web.Certificate/SignedAssertionFilePathCredentialsLoader.cs
@@ -5,11 +5,22 @@
using System.Threading;
using Microsoft.Identity.Abstractions;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
namespace Microsoft.Identity.Web
{
internal class SignedAssertionFilePathCredentialsLoader : ICredentialSourceLoader
{
+ ILogger? _logger;
+
+ ///
+ /// Constructor
+ ///
+ /// Optional logger.
+ public SignedAssertionFilePathCredentialsLoader(ILogger? logger)
+ {
+ _logger = logger;
+ }
public CredentialSource CredentialSource => CredentialSource.SignedAssertionFilePath;
public async Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? credentialSourceLoaderParameters)
@@ -19,17 +30,17 @@ public async Task LoadIfNeededAsync(CredentialDescription credentialDescription,
AzureIdentityForKubernetesClientAssertion? signedAssertion = credentialDescription.CachedValue as AzureIdentityForKubernetesClientAssertion;
if (credentialDescription.CachedValue == null)
{
- signedAssertion = new AzureIdentityForKubernetesClientAssertion(credentialDescription.SignedAssertionFileDiskPath);
+ signedAssertion = new AzureIdentityForKubernetesClientAssertion(credentialDescription.SignedAssertionFileDiskPath, _logger);
}
try
{
// Given that managed identity can be not available locally, we need to try to get a
// signed assertion, and if it fails, move to the next credentials
_= await signedAssertion!.GetSignedAssertion(CancellationToken.None);
+ credentialDescription.CachedValue = signedAssertion;
}
catch (Exception)
{
- credentialDescription.CachedValue = signedAssertion;
credentialDescription.Skip = true;
}
}
diff --git a/src/Microsoft.Identity.Web.Certificate/SignedAssertionFromManagedIdentityCredentialLoader.cs b/src/Microsoft.Identity.Web.Certificate/SignedAssertionFromManagedIdentityCredentialLoader.cs
index 9a3056cef..1067e1af4 100644
--- a/src/Microsoft.Identity.Web.Certificate/SignedAssertionFromManagedIdentityCredentialLoader.cs
+++ b/src/Microsoft.Identity.Web.Certificate/SignedAssertionFromManagedIdentityCredentialLoader.cs
@@ -30,10 +30,10 @@ public async Task LoadIfNeededAsync(CredentialDescription credentialDescription,
// Given that managed identity can be not available locally, we need to try to get a
// signed assertion, and if it fails, move to the next credentials
_= await managedIdentityClientAssertion!.GetSignedAssertion(CancellationToken.None);
+ credentialDescription.CachedValue = managedIdentityClientAssertion;
}
catch (AuthenticationFailedException)
{
- credentialDescription.CachedValue = managedIdentityClientAssertion;
credentialDescription.Skip = true;
throw;
}
diff --git a/src/Microsoft.Identity.Web.Certificateless/AzureIdentityForKubernetesClientAssertion.Logger.cs b/src/Microsoft.Identity.Web.Certificateless/AzureIdentityForKubernetesClientAssertion.Logger.cs
new file mode 100644
index 000000000..c62ad22a4
--- /dev/null
+++ b/src/Microsoft.Identity.Web.Certificateless/AzureIdentityForKubernetesClientAssertion.Logger.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Identity.Web
+{
+ public partial class AzureIdentityForKubernetesClientAssertion
+ {
+ /*
+ // High performance logger messages (before generation).
+ #pragma warning disable SYSLIB1009 // Logging methods must be static
+ [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "SignedAssertionFileDiskPath not provided. Falling back to the content of the AZURE_FEDERATED_TOKEN_FILE environment variable. ")]
+ partial void SignedAssertionFileDiskPathNotProvided(ILogger logger);
+
+ [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = "The `{environmentVariableName}` environment variable not provided. ")]
+ partial void SignedAssertionEnvironmentVariableNotProvided(ILogger logger, string environmentVariableName);
+
+ [LoggerMessage(EventId = 3, Level = LogLevel.Error, Message = "The environment variable AZURE_FEDERATED_TOKEN_FILE or AZURE_ACCESS_TOKEN_FILE or the 'SignedAssertionFileDiskPath' must be set to the path of the file containing the signed assertion. ")]
+ partial void NoSignedAssertionParameterProvided(ILogger logger);
+
+ [LoggerMessage(EventId = 4, Level = LogLevel.Error, Message = "The file `{filePath}` containing the signed assertion was not found. ")]
+ partial void FileAssertionPathNotFound(ILogger logger, string filePath);
+
+ [LoggerMessage(EventId = 5, Level = LogLevel.Information, Message = "Successfully read the signed assertion for `{filePath}`. Expires at {expiry}. ")]
+ partial void SuccessFullyReadSignedAssertion(ILogger logger, string filePath, DateTime expiry);
+
+ [LoggerMessage(EventId = 6, Level = LogLevel.Error, Message = "The file `{filePath} does not contain a valid signed assertion. {message}. ")]
+ partial void FileDoesNotContainValidAssertion(ILogger logger, string filePath, string message);
+ #pragma warning restore SYSLIB1009 // Logging methods must be static
+ */
+
+ ///
+ /// Performant logging messages.
+ ///
+ static class Logger
+ {
+ public static void SignedAssertionFileDiskPathNotProvided(ILogger? logger)
+ {
+ if (logger != null && logger.IsEnabled(LogLevel.Information))
+ {
+ __SignedAssertionFileDiskPathNotProvidedCallback(logger, null);
+ }
+ }
+
+ public static void SignedAssertionEnvironmentVariableNotProvided(ILogger? logger, string environmentVariableName)
+ {
+ if (logger != null && logger.IsEnabled(LogLevel.Information))
+ {
+ __SignedAssertionEnvironmentVariableNotProvidedCallback(logger, environmentVariableName, null);
+ }
+ }
+
+ public static void NoSignedAssertionParameterProvided(ILogger? logger)
+ {
+ if (logger != null && logger.IsEnabled(LogLevel.Error))
+ {
+ __NoSignedAssertionParameterProvidedCallback(logger, null);
+ }
+ }
+
+ public static void FileAssertionPathNotFound(ILogger? logger, string filePath)
+ {
+ if (logger != null && logger.IsEnabled(LogLevel.Error))
+ {
+ __FileAssertionPathNotFoundCallback(logger, filePath, null);
+ }
+ }
+
+ public static void SuccessFullyReadSignedAssertion(ILogger? logger, string filePath, DateTime expiry)
+ {
+ if (logger != null && logger.IsEnabled(LogLevel.Information))
+ {
+ __SuccessFullyReadSignedAssertionCallback(logger, filePath, expiry, null);
+ }
+ }
+
+ public static void FileDoesNotContainValidAssertion(ILogger? logger, string filePath, string message)
+ {
+ if (logger != null && logger.IsEnabled(LogLevel.Error))
+ {
+ __FileDoesNotContainValidAssertionCallback(logger, filePath, message, null);
+ }
+ }
+
+ private static readonly Action __SignedAssertionFileDiskPathNotProvidedCallback =
+ LoggerMessage.Define(LogLevel.Information, new EventId(1, nameof(SignedAssertionFileDiskPathNotProvided)), "SignedAssertionFileDiskPath not provided. Falling back to the content of the AZURE_FEDERATED_TOKEN_FILE environment variable. ");
+ private static readonly Action __SignedAssertionEnvironmentVariableNotProvidedCallback =
+ LoggerMessage.Define(LogLevel.Information, new EventId(2, nameof(SignedAssertionEnvironmentVariableNotProvided)), "The `{environmentVariableName}` environment variable not provided. ");
+
+ private static readonly Action __NoSignedAssertionParameterProvidedCallback =
+ LoggerMessage.Define(LogLevel.Error, new EventId(3, nameof(NoSignedAssertionParameterProvided)), "The environment variable AZURE_FEDERATED_TOKEN_FILE or AZURE_ACCESS_TOKEN_FILE or the 'SignedAssertionFileDiskPath' must be set to the path of the file containing the signed assertion. ");
+
+ private static readonly Action __FileAssertionPathNotFoundCallback =
+ LoggerMessage.Define(LogLevel.Error, new EventId(4, nameof(FileAssertionPathNotFound)), "The file `{filePath}` containing the signed assertion was not found. ");
+
+ private static readonly Action __SuccessFullyReadSignedAssertionCallback =
+ LoggerMessage.Define(LogLevel.Information, new EventId(5, nameof(SuccessFullyReadSignedAssertion)), "Successfully read the signed assertion for `{filePath}`. Expires at {expiry}. ");
+
+ private static readonly Action __FileDoesNotContainValidAssertionCallback =
+ LoggerMessage.Define(LogLevel.Error, new EventId(6, nameof(FileDoesNotContainValidAssertion)), "The file `{filePath} does not contain a valid signed assertion. {message}. ");
+ }
+ }
+}
diff --git a/src/Microsoft.Identity.Web.Certificateless/AzureIdentityForKubernetesClientAssertion.cs b/src/Microsoft.Identity.Web.Certificateless/AzureIdentityForKubernetesClientAssertion.cs
index 96bb346a7..37d5aeeb3 100644
--- a/src/Microsoft.Identity.Web.Certificateless/AzureIdentityForKubernetesClientAssertion.cs
+++ b/src/Microsoft.Identity.Web.Certificateless/AzureIdentityForKubernetesClientAssertion.cs
@@ -5,6 +5,7 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.JsonWebTokens;
namespace Microsoft.Identity.Web
@@ -14,14 +15,19 @@ namespace Microsoft.Identity.Web
/// in Azure Kubernetes Services. See https://aka.ms/ms-id-web/certificateless and
/// https://learn.microsoft.com/azure/aks/workload-identity-overview
///
- public class AzureIdentityForKubernetesClientAssertion : ClientAssertionProviderBase
+ public partial class AzureIdentityForKubernetesClientAssertion : ClientAssertionProviderBase
{
+ const string azureAccessTokenFileEnvironmentVariable = "AZURE_ACCESS_TOKEN_FILE";
+ const string azureFederatedTokenFileEnvironmentVariable = "AZURE_FEDERATED_TOKEN_FILE";
+ private readonly string? _filePath;
+ private readonly ILogger? _logger;
+
///
/// Gets a signed assertion from Azure workload identity for kubernetes. The file path is provided
/// by an environment variable ("AZURE_FEDERATED_TOKEN_FILE")
/// See https://aka.ms/ms-id-web/certificateless.
///
- public AzureIdentityForKubernetesClientAssertion() : this(null)
+ public AzureIdentityForKubernetesClientAssertion(ILogger? logger = null) : this(null, logger)
{
}
@@ -29,25 +35,60 @@ public AzureIdentityForKubernetesClientAssertion() : this(null)
/// Gets a signed assertion from a file.
/// See https://aka.ms/ms-id-web/certificateless.
///
- ///
- public AzureIdentityForKubernetesClientAssertion(string? filePath)
+ /// Path to a file containing the signed assertion.
+ /// Logger.
+ public AzureIdentityForKubernetesClientAssertion(string? filePath, ILogger? logger = null)
{
+ _logger = logger;
+
+ if (filePath == null)
+ {
+ Logger.SignedAssertionFileDiskPathNotProvided(_logger);
+ }
+
+ _filePath = _filePath ?? Environment.GetEnvironmentVariable(azureAccessTokenFileEnvironmentVariable);
+ if (filePath == null)
+ {
+ Logger.SignedAssertionEnvironmentVariableNotProvided(_logger, azureAccessTokenFileEnvironmentVariable);
+ }
+
// See https://blog.identitydigest.com/azuread-federate-k8s/
- _filePath = filePath ?? Environment.GetEnvironmentVariable("AZURE_FEDERATED_TOKEN_FILE");
+ _filePath = filePath ?? Environment.GetEnvironmentVariable(azureFederatedTokenFileEnvironmentVariable);
+ if (_filePath == null)
+ {
+ Logger.SignedAssertionEnvironmentVariableNotProvided(_logger, azureFederatedTokenFileEnvironmentVariable);
+ Logger.NoSignedAssertionParameterProvided(_logger);
+ }
}
- private readonly string _filePath;
-
///
/// Get the signed assertion from a file.
///
/// The signed assertion.
protected override Task GetClientAssertion(CancellationToken cancellationToken)
{
+ if (_filePath != null && !File.Exists(_filePath))
+ {
+ Logger.FileAssertionPathNotFound(_logger, _filePath);
+ throw new FileNotFoundException($"The file '{_filePath}' containing the signed assertion was not found.");
+
+ }
string signedAssertion = File.ReadAllText(_filePath);
- // Compute the expiry
- JsonWebToken jwt = new JsonWebToken(signedAssertion);
- return Task.FromResult(new ClientAssertion(signedAssertion, jwt.ValidTo));
+
+ // Verify that the assertion is a JWS, JWE, and computes the expiry
+ try
+ {
+ JsonWebToken jwt = new JsonWebToken(signedAssertion);
+
+ Logger.SuccessFullyReadSignedAssertion(_logger, _filePath!, jwt.ValidTo);
+
+ return Task.FromResult(new ClientAssertion(signedAssertion, jwt.ValidTo));
+ }
+ catch (ArgumentException ex)
+ {
+ Logger.FileDoesNotContainValidAssertion(_logger, _filePath!, ex.Message);
+ throw;
+ }
}
}
}
diff --git a/src/Microsoft.Identity.Web.Certificateless/Microsoft.Identity.Web.Certificateless.csproj b/src/Microsoft.Identity.Web.Certificateless/Microsoft.Identity.Web.Certificateless.csproj
index e9ade8f95..8bb144a0f 100644
--- a/src/Microsoft.Identity.Web.Certificateless/Microsoft.Identity.Web.Certificateless.csproj
+++ b/src/Microsoft.Identity.Web.Certificateless/Microsoft.Identity.Web.Certificateless.csproj
@@ -12,6 +12,7 @@
+
diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs b/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs
index 0fd7c6d76..7a1dc889f 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/Constants.cs
@@ -128,6 +128,7 @@ public static class Constants
internal const string True = "True";
internal const string InvalidClient = "invalid_client";
internal const string InvalidKeyError = "AADSTS700027";
+ internal const string SignedAssertionInvalidTimeRange = "AADSTS700024";
internal const string CiamAuthoritySuffix = ".ciamlogin.com";
internal const string TestSlice = "dc";
diff --git a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
index d412ede3a..2d4153d2f 100644
--- a/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
+++ b/src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquisition.cs
@@ -167,7 +167,7 @@ public async Task AddAccountToCacheFromAuthorizationCodeAsyn
result.CorrelationId,
result.TokenType);
}
- catch (MsalServiceException exMsal) when (IsInvalidClientCertificateError(exMsal))
+ catch (MsalServiceException exMsal) when (IsInvalidClientCertificateOrSignedAssertionError(exMsal))
{
DefaultCertificateLoader.ResetCertificates(mergedOptions.ClientCertificates);
_applicationsByAuthorityClientId[GetApplicationKey(mergedOptions)] = null;
@@ -262,7 +262,7 @@ public async Task GetAuthenticationResultForUserAsync(
LogAuthResult(authenticationResult);
return authenticationResult;
}
- catch (MsalServiceException exMsal) when (IsInvalidClientCertificateError(exMsal))
+ catch (MsalServiceException exMsal) when (IsInvalidClientCertificateOrSignedAssertionError(exMsal))
{
DefaultCertificateLoader.ResetCertificates(mergedOptions.ClientCertificates);
_applicationsByAuthorityClientId[GetApplicationKey(mergedOptions)] = null;
@@ -356,13 +356,13 @@ public Task GetAuthenticationResultForAppAsync(
// MSAL.net only allows .WithTenantId for AAD authorities. This makes sense as there should
// not be cross tenant operations with such an authority.
- if (!mergedOptions.Instance.Contains(".ciamlogin.com"
+ if (!mergedOptions.Instance.Contains(Constants.CiamAuthoritySuffix
#if NETCOREAPP3_1_OR_GREATER
, StringComparison.OrdinalIgnoreCase
#endif
))
- {
- builder.WithTenantId(tenant);
+ {
+ builder.WithTenantId(tenant);
}
if (tokenAcquisitionOptions != null)
@@ -414,7 +414,7 @@ public Task GetAuthenticationResultForAppAsync(
{
return builder.ExecuteAsync(tokenAcquisitionOptions != null ? tokenAcquisitionOptions.CancellationToken : CancellationToken.None);
}
- catch (MsalServiceException exMsal) when (IsInvalidClientCertificateError(exMsal))
+ catch (MsalServiceException exMsal) when (IsInvalidClientCertificateOrSignedAssertionError(exMsal))
{
DefaultCertificateLoader.ResetCertificates(mergedOptions.ClientCertificates);
_applicationsByAuthorityClientId[GetApplicationKey(mergedOptions)] = null;
@@ -540,14 +540,15 @@ public async Task RemoveAccountAsync(
}
}
- private bool IsInvalidClientCertificateError(MsalServiceException exMsal)
+ private bool IsInvalidClientCertificateOrSignedAssertionError(MsalServiceException exMsal)
{
return !_retryClientCertificate &&
string.Equals(exMsal.ErrorCode, Constants.InvalidClient, StringComparison.OrdinalIgnoreCase) &&
#if !NETSTANDARD2_0 && !NET462 && !NET472
- exMsal.Message.Contains(Constants.InvalidKeyError, StringComparison.OrdinalIgnoreCase);
+ (exMsal.Message.Contains(Constants.InvalidKeyError, StringComparison.OrdinalIgnoreCase)
+ || exMsal.Message.Contains(Constants.SignedAssertionInvalidTimeRange, StringComparison.OrdinalIgnoreCase));
#else
- exMsal.Message.Contains(Constants.InvalidKeyError);
+ (exMsal.Message.Contains(Constants.InvalidKeyError) || exMsal.Message.Contains(Constants.SignedAssertionInvalidTimeRange));
#endif
}
diff --git a/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs
index 9d8ad5f88..ac61d7da6 100644
--- a/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs
+++ b/tests/Microsoft.Identity.Web.Test.Common/TestConstants.cs
@@ -216,5 +216,7 @@ public static class TestConstants
""login-us.microsoftonline.com""]}
]
}";
+
+ public const string signedAssertionFilePath = "signedAssertion.txt";
}
}
diff --git a/tests/Microsoft.Identity.Web.Test/AzureIdentityForKubernetesClientAssertionTests.cs b/tests/Microsoft.Identity.Web.Test/AzureIdentityForKubernetesClientAssertionTests.cs
new file mode 100644
index 000000000..1804ce4b7
--- /dev/null
+++ b/tests/Microsoft.Identity.Web.Test/AzureIdentityForKubernetesClientAssertionTests.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Web.Test.Common;
+using Microsoft.IdentityModel.JsonWebTokens;
+using Xunit;
+
+namespace Microsoft.Identity.Web.Tests.Certificateless
+{
+ public class AzureIdentityForKubernetesClientAssertionTests
+ {
+ string token;
+
+ public AzureIdentityForKubernetesClientAssertionTests()
+ {
+ JsonWebTokenHandler handler = new JsonWebTokenHandler();
+ token = handler.CreateToken("{}");
+ }
+
+ [Fact]
+ public async Task GetAksClientAssertion_WhenSpecifiedSignedAssertionFileExists_ReturnsClientAssertion()
+ {
+ // Arrange
+ File.WriteAllText(TestConstants.signedAssertionFilePath, token.ToString());
+ AzureIdentityForKubernetesClientAssertion azureIdentityForKubernetesClientAssertion = new AzureIdentityForKubernetesClientAssertion(TestConstants.signedAssertionFilePath);
+
+ // Act
+ string signedAssertion = await azureIdentityForKubernetesClientAssertion.GetSignedAssertion(CancellationToken.None);
+
+ // Assert
+ Assert.NotNull(signedAssertion);
+
+ // Delete the signed assertion file.
+ File.Delete(TestConstants.signedAssertionFilePath);
+ }
+
+ [Fact]
+ public async Task GetAksClientAssertion_WhenEnvironmentVariablePointsToSignedAssertionFileExists_ReturnsClientAssertion()
+ {
+ // Arrange
+ File.WriteAllText(TestConstants.signedAssertionFilePath, token.ToString());
+ Environment.SetEnvironmentVariable("AZURE_FEDERATED_TOKEN_FILE", TestConstants.signedAssertionFilePath);
+ AzureIdentityForKubernetesClientAssertion azureIdentityForKubernetesClientAssertion = new AzureIdentityForKubernetesClientAssertion();
+
+ // Act
+ string signedAssertion = await azureIdentityForKubernetesClientAssertion.GetSignedAssertion(CancellationToken.None);
+
+ // Assert
+ Assert.NotNull(signedAssertion);
+
+ // Delete the signed assertion file and remove the environment variable.
+ File.Delete(TestConstants.signedAssertionFilePath);
+ Environment.SetEnvironmentVariable("AZURE_FEDERATED_TOKEN_FILE", null);
+ }
+
+ [Fact]
+ public async Task GetAksClientAssertion_WhenSignedAssertionFileDoesNotExist_ThrowsFileNotFoundException()
+ {
+ // Arrange
+ var filePath = "doesNotExist.txt";
+ AzureIdentityForKubernetesClientAssertion azureIdentityForKubernetesClientAssertion = new AzureIdentityForKubernetesClientAssertion(filePath);
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => azureIdentityForKubernetesClientAssertion.GetSignedAssertion(CancellationToken.None));
+ Assert.Contains(filePath, ex.Message, System.StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/tests/Microsoft.Identity.Web.Test/SignedAssertionFilePathCredentialsLoader.cs b/tests/Microsoft.Identity.Web.Test/SignedAssertionFilePathCredentialsLoader.cs
new file mode 100644
index 000000000..1297a358b
--- /dev/null
+++ b/tests/Microsoft.Identity.Web.Test/SignedAssertionFilePathCredentialsLoader.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Identity.Abstractions;
+using Microsoft.Identity.Web.Test.Common;
+using Microsoft.IdentityModel.JsonWebTokens;
+using Xunit;
+
+namespace Microsoft.Identity.Web.Tests.Certificateless
+{
+ public class SignedAssertionFilePathCredentialsLoaderTests
+ {
+ const string filePath = "signedAssertion.txt";
+ const string aksEnvironmentVariableName = "AZURE_FEDERATED_TOKEN_FILE";
+ string token;
+ SignedAssertionFilePathCredentialsLoader signedAssertionFilePathCredentialsLoader = new SignedAssertionFilePathCredentialsLoader(null);
+
+
+ public SignedAssertionFilePathCredentialsLoaderTests()
+ {
+ JsonWebTokenHandler handler = new JsonWebTokenHandler();
+ token = handler.CreateToken("{}");
+ }
+
+ [Fact]
+ public async Task GetClientAssertion_WhenSpecifiedSignedAssertionFileExists_ReturnsClientAssertion()
+ {
+ // Arrange
+ File.WriteAllText(filePath, token.ToString());
+ CredentialDescription credentialDescription = new CredentialDescription
+ {
+ SourceType = CredentialSource.SignedAssertionFilePath,
+ SignedAssertionFileDiskPath = filePath
+ };
+
+ // Act
+ await signedAssertionFilePathCredentialsLoader.LoadIfNeededAsync(credentialDescription, null);
+
+ // Assert
+ Assert.NotNull(credentialDescription.CachedValue);
+
+ // Delete the signed assertion file.
+ File.Delete(filePath);
+ }
+
+ [Fact]
+ public async Task GetClientAssertion_WhenEnvironmentVariablePointsToSignedAssertionFileExists_ReturnsClientAssertion()
+ {
+ // Arrange
+ File.WriteAllText(filePath, token.ToString());
+ Environment.SetEnvironmentVariable(aksEnvironmentVariableName, filePath);
+ CredentialDescription credentialDescription = new CredentialDescription
+ {
+ SourceType = CredentialSource.SignedAssertionFilePath,
+ };
+
+ // Act
+ await signedAssertionFilePathCredentialsLoader.LoadIfNeededAsync(credentialDescription, null);
+
+ // Assert
+ Assert.NotNull(credentialDescription.CachedValue);
+
+ // Delete the signed assertion file and remove the environment variable.
+ File.Delete(filePath);
+ Environment.SetEnvironmentVariable(aksEnvironmentVariableName, null);
+ }
+
+ [Fact]
+ public async Task GetClientAssertion_WhenSignedAssertionFileDoesNotExist_ThrowsFileNotFoundException()
+ {
+ // Act
+ CredentialDescription credentialDescription = new CredentialDescription
+ {
+ SourceType = CredentialSource.SignedAssertionFilePath,
+ };
+ await signedAssertionFilePathCredentialsLoader.LoadIfNeededAsync(credentialDescription, null);
+
+ // Act & Assert
+ Assert.Null(credentialDescription.CachedValue);
+ }
+ }
+}