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); + } + } +}