diff --git a/src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs b/src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs index a5533ce2f6..7ce35c6d25 100644 --- a/src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs +++ b/src/client/Microsoft.Identity.Client/AuthScheme/PoP/MtlsPopAuthenticationOperation.cs @@ -38,7 +38,7 @@ public IReadOnlyDictionary GetTokenRequestParams() public void FormatResult(AuthenticationResult authenticationResult) { - //no-op + authenticationResult.BindingCertificate = _mtlsCert; } private static string ComputeX5tS256KeyId(X509Certificate2 certificate) diff --git a/src/client/Microsoft.Identity.Client/AuthenticationResult.cs b/src/client/Microsoft.Identity.Client/AuthenticationResult.cs index c75c921375..de47a9b4f2 100644 --- a/src/client/Microsoft.Identity.Client/AuthenticationResult.cs +++ b/src/client/Microsoft.Identity.Client/AuthenticationResult.cs @@ -12,6 +12,7 @@ using Microsoft.Identity.Client.Cache.Items; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; using Microsoft.Identity.Client.Utils; +using System.Security.Cryptography.X509Certificates; namespace Microsoft.Identity.Client { @@ -42,6 +43,8 @@ public partial class AuthenticationResult /// Claims from the ID token /// Auth Code returned by the Microsoft identity platform when you use AcquireTokenByAuthorizationCode.WithSpaAuthorizationCode(). This auth code is meant to be redeemed by the frontend code. See https://aka.ms/msal-net/spa-auth-code /// Other properties from the token response. + [Obsolete("Direct constructor use is deprecated.", error: false)] + [EditorBrowsable(EditorBrowsableState.Never)] public AuthenticationResult( // for backwards compat with 4.16- string accessToken, bool isExtendedLifeTimeToken, @@ -95,6 +98,7 @@ public partial class AuthenticationResult /// Contains metadata related to the Authentication Result. /// The token type, defaults to Bearer. Note: this property is experimental and may change in future versions of the library. /// For backwards compatibility with MSAL 4.17-4.20 + [Obsolete("Direct constructor use is deprecated.", error: false)] [EditorBrowsable(EditorBrowsableState.Never)] public AuthenticationResult( string accessToken, @@ -234,14 +238,14 @@ internal AuthenticationResult() { } /// Guest AAD accounts have different oid claim values in each tenant. Use to uniquely identify users across tenants. /// See https://docs.microsoft.com/azure/active-directory/develop/id-tokens#payload-claims /// - public string UniqueId { get; } + public string UniqueId { get; set; } /// /// Gets the point in time in which the Access Token returned in the property ceases to be valid. /// This value is calculated based on current UTC time measured locally and the value expiresIn received from the /// service. /// - public DateTimeOffset ExpiresOn { get; } + public DateTimeOffset ExpiresOn { get; set; } /// /// Gets the point in time in which the Access Token returned in the AccessToken property ceases to be valid in MSAL's extended LifeTime. @@ -255,7 +259,7 @@ internal AuthenticationResult() { } /// Gets an identifier for the Azure AD tenant from which the token was acquired. This property will be null if tenant information is /// not returned by the service. /// - public string TenantId { get; } + public string TenantId { get; set; } /// /// Gets the account information. Some elements in might be null if not returned by the @@ -263,34 +267,39 @@ internal AuthenticationResult() { } /// as or /// for instance /// - public IAccount Account { get; } + public IAccount Account { get; set; } /// /// Gets the Id Token if returned by the service or null if no Id Token is returned. /// - public string IdToken { get; } + public string IdToken { get; set; } /// /// Gets the granted scope values returned by the service. /// - public IEnumerable Scopes { get; } + public IEnumerable Scopes { get; set; } /// /// Gets the correlation id used for the request. /// - public Guid CorrelationId { get; } + public Guid CorrelationId { get; set; } /// /// Identifies the type of access token. By default tokens returned by Azure Active Directory are Bearer tokens. /// for getting an HTTP authorization header from an AuthenticationResult. /// - public string TokenType { get; } + public string TokenType { get; set; } /// /// Gets the SPA Authorization Code, if it was requested using WithSpaAuthorizationCode method on the /// AcquireTokenByAuthorizationCode builder. See https://aka.ms/msal-net/spa-auth-code for details. /// - public string SpaAuthCode { get; } + public string SpaAuthCode { get; set; } + + /// + /// The X509 certificate bound to the access-token when mTLS-PoP was used. + /// + public X509Certificate2 BindingCertificate { get; set; } /// /// Exposes additional response parameters returned by the token issuer (AAD). @@ -299,19 +308,19 @@ internal AuthenticationResult() { } /// Not all parameters are added here, only the ones that MSAL doesn't interpret itself and only scalars. /// Not supported on mobile frameworks (e.g. net8-android or net8-ios) /// - public IReadOnlyDictionary AdditionalResponseParameters { get; } + public IReadOnlyDictionary AdditionalResponseParameters { get; set; } /// /// All the claims present in the ID token. /// - public ClaimsPrincipal ClaimsPrincipal { get; } + public ClaimsPrincipal ClaimsPrincipal { get; set; } - internal ApiEvent ApiEvent { get; } + internal ApiEvent ApiEvent { get; set; } /// /// Contains metadata for the Authentication result. /// - public AuthenticationResultMetadata AuthenticationResultMetadata { get; } + public AuthenticationResultMetadata AuthenticationResultMetadata { get; set; } /// /// Creates the content for an HTTP authorization header from this authentication result, so diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index 50d9f12956..2463f00ac3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -1,2 +1,16 @@ const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string const Microsoft.Identity.Client.MsalError.MissingManagedIdentityEnvVar = "missing_managed_identity_env_var" -> string +Microsoft.Identity.Client.AuthenticationResult.Account.set -> void +Microsoft.Identity.Client.AuthenticationResult.AdditionalResponseParameters.set -> void +Microsoft.Identity.Client.AuthenticationResult.AuthenticationResultMetadata.set -> void +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.set -> void +Microsoft.Identity.Client.AuthenticationResult.ClaimsPrincipal.set -> void +Microsoft.Identity.Client.AuthenticationResult.CorrelationId.set -> void +Microsoft.Identity.Client.AuthenticationResult.ExpiresOn.set -> void +Microsoft.Identity.Client.AuthenticationResult.IdToken.set -> void +Microsoft.Identity.Client.AuthenticationResult.Scopes.set -> void +Microsoft.Identity.Client.AuthenticationResult.SpaAuthCode.set -> void +Microsoft.Identity.Client.AuthenticationResult.TenantId.set -> void +Microsoft.Identity.Client.AuthenticationResult.TokenType.set -> void +Microsoft.Identity.Client.AuthenticationResult.UniqueId.set -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index 50d9f12956..2463f00ac3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -1,2 +1,16 @@ const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string const Microsoft.Identity.Client.MsalError.MissingManagedIdentityEnvVar = "missing_managed_identity_env_var" -> string +Microsoft.Identity.Client.AuthenticationResult.Account.set -> void +Microsoft.Identity.Client.AuthenticationResult.AdditionalResponseParameters.set -> void +Microsoft.Identity.Client.AuthenticationResult.AuthenticationResultMetadata.set -> void +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.set -> void +Microsoft.Identity.Client.AuthenticationResult.ClaimsPrincipal.set -> void +Microsoft.Identity.Client.AuthenticationResult.CorrelationId.set -> void +Microsoft.Identity.Client.AuthenticationResult.ExpiresOn.set -> void +Microsoft.Identity.Client.AuthenticationResult.IdToken.set -> void +Microsoft.Identity.Client.AuthenticationResult.Scopes.set -> void +Microsoft.Identity.Client.AuthenticationResult.SpaAuthCode.set -> void +Microsoft.Identity.Client.AuthenticationResult.TenantId.set -> void +Microsoft.Identity.Client.AuthenticationResult.TokenType.set -> void +Microsoft.Identity.Client.AuthenticationResult.UniqueId.set -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index 50d9f12956..2463f00ac3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -1,2 +1,16 @@ const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string const Microsoft.Identity.Client.MsalError.MissingManagedIdentityEnvVar = "missing_managed_identity_env_var" -> string +Microsoft.Identity.Client.AuthenticationResult.Account.set -> void +Microsoft.Identity.Client.AuthenticationResult.AdditionalResponseParameters.set -> void +Microsoft.Identity.Client.AuthenticationResult.AuthenticationResultMetadata.set -> void +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.set -> void +Microsoft.Identity.Client.AuthenticationResult.ClaimsPrincipal.set -> void +Microsoft.Identity.Client.AuthenticationResult.CorrelationId.set -> void +Microsoft.Identity.Client.AuthenticationResult.ExpiresOn.set -> void +Microsoft.Identity.Client.AuthenticationResult.IdToken.set -> void +Microsoft.Identity.Client.AuthenticationResult.Scopes.set -> void +Microsoft.Identity.Client.AuthenticationResult.SpaAuthCode.set -> void +Microsoft.Identity.Client.AuthenticationResult.TenantId.set -> void +Microsoft.Identity.Client.AuthenticationResult.TokenType.set -> void +Microsoft.Identity.Client.AuthenticationResult.UniqueId.set -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index 50d9f12956..2463f00ac3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -1,2 +1,16 @@ const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string const Microsoft.Identity.Client.MsalError.MissingManagedIdentityEnvVar = "missing_managed_identity_env_var" -> string +Microsoft.Identity.Client.AuthenticationResult.Account.set -> void +Microsoft.Identity.Client.AuthenticationResult.AdditionalResponseParameters.set -> void +Microsoft.Identity.Client.AuthenticationResult.AuthenticationResultMetadata.set -> void +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.set -> void +Microsoft.Identity.Client.AuthenticationResult.ClaimsPrincipal.set -> void +Microsoft.Identity.Client.AuthenticationResult.CorrelationId.set -> void +Microsoft.Identity.Client.AuthenticationResult.ExpiresOn.set -> void +Microsoft.Identity.Client.AuthenticationResult.IdToken.set -> void +Microsoft.Identity.Client.AuthenticationResult.Scopes.set -> void +Microsoft.Identity.Client.AuthenticationResult.SpaAuthCode.set -> void +Microsoft.Identity.Client.AuthenticationResult.TenantId.set -> void +Microsoft.Identity.Client.AuthenticationResult.TokenType.set -> void +Microsoft.Identity.Client.AuthenticationResult.UniqueId.set -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index 50d9f12956..2463f00ac3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -1,2 +1,16 @@ const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string const Microsoft.Identity.Client.MsalError.MissingManagedIdentityEnvVar = "missing_managed_identity_env_var" -> string +Microsoft.Identity.Client.AuthenticationResult.Account.set -> void +Microsoft.Identity.Client.AuthenticationResult.AdditionalResponseParameters.set -> void +Microsoft.Identity.Client.AuthenticationResult.AuthenticationResultMetadata.set -> void +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.set -> void +Microsoft.Identity.Client.AuthenticationResult.ClaimsPrincipal.set -> void +Microsoft.Identity.Client.AuthenticationResult.CorrelationId.set -> void +Microsoft.Identity.Client.AuthenticationResult.ExpiresOn.set -> void +Microsoft.Identity.Client.AuthenticationResult.IdToken.set -> void +Microsoft.Identity.Client.AuthenticationResult.Scopes.set -> void +Microsoft.Identity.Client.AuthenticationResult.SpaAuthCode.set -> void +Microsoft.Identity.Client.AuthenticationResult.TenantId.set -> void +Microsoft.Identity.Client.AuthenticationResult.TokenType.set -> void +Microsoft.Identity.Client.AuthenticationResult.UniqueId.set -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index 50d9f12956..2463f00ac3 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,2 +1,16 @@ const Microsoft.Identity.Client.MsalError.InvalidManagedIdentityIdType = "invalid_managed_identity_id_type" -> string const Microsoft.Identity.Client.MsalError.MissingManagedIdentityEnvVar = "missing_managed_identity_env_var" -> string +Microsoft.Identity.Client.AuthenticationResult.Account.set -> void +Microsoft.Identity.Client.AuthenticationResult.AdditionalResponseParameters.set -> void +Microsoft.Identity.Client.AuthenticationResult.AuthenticationResultMetadata.set -> void +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2 +Microsoft.Identity.Client.AuthenticationResult.BindingCertificate.set -> void +Microsoft.Identity.Client.AuthenticationResult.ClaimsPrincipal.set -> void +Microsoft.Identity.Client.AuthenticationResult.CorrelationId.set -> void +Microsoft.Identity.Client.AuthenticationResult.ExpiresOn.set -> void +Microsoft.Identity.Client.AuthenticationResult.IdToken.set -> void +Microsoft.Identity.Client.AuthenticationResult.Scopes.set -> void +Microsoft.Identity.Client.AuthenticationResult.SpaAuthCode.set -> void +Microsoft.Identity.Client.AuthenticationResult.TenantId.set -> void +Microsoft.Identity.Client.AuthenticationResult.TokenType.set -> void +Microsoft.Identity.Client.AuthenticationResult.UniqueId.set -> void diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs index 0810388ff7..80cc1c97f8 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsMtlsPopTests.cs @@ -57,6 +57,11 @@ public async Task Sni_Gets_Pop_Token_Successfully_TestAsync() Assert.AreEqual(Constants.MtlsPoPTokenType, authResult.TokenType, "Token type should be MTLS PoP"); Assert.IsNotNull(authResult.AccessToken, "Access token should not be null"); + Assert.IsNotNull(authResult.BindingCertificate, "BindingCertificate should be set in SNI flow."); + Assert.AreEqual(cert.Thumbprint, + authResult.BindingCertificate.Thumbprint, + "BindingCertificate must match the certificate supplied via WithCertificate()."); + // Simulate cache retrieval to verify MTLS configuration is cached properly authResult = await confidentialApp .AcquireTokenForClient(settings.AppScopes) @@ -66,6 +71,11 @@ public async Task Sni_Gets_Pop_Token_Successfully_TestAsync() // Assert: Verify that the token was fetched from cache on the second request Assert.AreEqual(TokenSource.Cache, authResult.AuthenticationResultMetadata.TokenSource, "Token should be retrieved from cache"); + + Assert.IsNotNull(authResult.BindingCertificate, "BindingCertificate should be set in SNI flow."); + Assert.AreEqual(cert.Thumbprint, + authResult.BindingCertificate.Thumbprint, + "BindingCertificate must match the certificate supplied via WithCertificate()."); } } } diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsTests.NetFwk.cs b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsTests.NetFwk.cs index 6824b28155..3e1eb4616c 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsTests.NetFwk.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/HeadlessTests/ClientCredentialsTests.NetFwk.cs @@ -312,6 +312,9 @@ private async Task RunClientCredsAsync(Cloud cloud, CredentialType credentialTyp GetExpectedCacheKey(settings.ClientId, settings.TenantId), appCacheRecorder.LastAfterAccessNotificationArgs.SuggestedCacheKey); + Assert.IsNull(authResult.BindingCertificate, + "BindingCertificate should be null for bearer tokens."); + // Call again to ensure token cache is hit authResult = await confidentialApp .AcquireTokenForClient(settings.AppScopes) @@ -330,6 +333,9 @@ private async Task RunClientCredsAsync(Cloud cloud, CredentialType credentialTyp Assert.AreEqual( GetExpectedCacheKey(settings.ClientId, settings.TenantId), appCacheRecorder.LastAfterAccessNotificationArgs.SuggestedCacheKey); + + Assert.IsNull(authResult.BindingCertificate, + "BindingCertificate should be null for bearer tokens."); } private static IConfidentialClientApplication CreateApp( diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationResultTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationResultTests.cs index 9be68bf4fc..a98e714373 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationResultTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/AuthenticationResultTests.cs @@ -18,25 +18,10 @@ namespace Microsoft.Identity.Test.Unit.PublicApiTests public class AuthenticationResultTests : TestBase { - [TestMethod] - public void PublicTestConstructorCoversAllProperties() - { - var ctorParameters = typeof(AuthenticationResult) - .GetConstructors() - .First(ctor => ctor.GetParameters().Length > 3) - .GetParameters(); - - var classProperties = typeof(AuthenticationResult) - .GetProperties() - .Where(p => p.GetCustomAttribute(typeof(ObsoleteAttribute)) == null); - - // +2 because of the obsolete ExtendedExpires properties - Assert.AreEqual(ctorParameters.Length, classProperties.Count() + 2, "The constructor should include all properties of AuthenticationObject"); ; - } - [TestMethod] public void GetAuthorizationHeader() { +#pragma warning disable CS0618 // exercising obsolete constructors until full deprecation var ar = new AuthenticationResult( "at", false, @@ -79,6 +64,7 @@ public void GetHybridSpaAuthCode() "one for backwards compat with 4.17+ and one for 4.16 and below")] public void AuthenticationResult_PublicApi() { +#pragma warning disable CS0618 // exercising obsolete constructors until full deprecation // old constructor, before 4.16 var ar1 = new AuthenticationResult( "at", @@ -88,8 +74,8 @@ public void AuthenticationResult_PublicApi() DateTime.UtcNow, "tid", new Account("aid", "user", "env"), - "idt", - new[] { "scope" }, + "idt", + new[] { "scope" }, Guid.NewGuid()); Assert.IsNull(ar1.AuthenticationResultMetadata); @@ -106,7 +92,7 @@ public void AuthenticationResult_PublicApi() new Account("aid", "user", "env"), "idt", new[] { "scope" }, - Guid.NewGuid(), + Guid.NewGuid(), "ProofOfBear"); Assert.IsNull(ar2.AuthenticationResultMetadata); @@ -124,7 +110,8 @@ public void AuthenticationResult_PublicApi() "idt", new[] { "scope" }, Guid.NewGuid(), - new AuthenticationResultMetadata(TokenSource.Broker)); + new AuthenticationResultMetadata(TokenSource.Broker), + tokenType: "Bearer"); Assert.AreEqual(TokenSource.Broker, ar3.AuthenticationResultMetadata.TokenSource); Assert.AreEqual("Bearer", ar1.TokenType); @@ -144,7 +131,7 @@ public async Task MsalTokenResponseParseTestAsync() string jsonContent = MockHelpers.CreateSuccessTokenResponseString( TestConstants.Uid, - TestConstants.DisplayableId, + TestConstants.DisplayableId, TestConstants.s_scope.ToArray()); jsonContent = jsonContent.TrimEnd('}'); @@ -216,5 +203,52 @@ public void DefaultTokenType_IsBearer_Test() Assert.AreEqual("Bearer", ar.TokenType, "Expected default token type to be 'Bearer'"); Assert.AreEqual("Bearer some-access-token", ar.CreateAuthorizationHeader()); } + + /// + /// Tests that all public properties of AuthenticationResult have a public setter. + /// + [TestMethod] + public void AllPublicProperties_HavePublicSetter() + { + // ---- expected public-settable properties ---- + string[] expected = + [ + // core primitives + nameof(AuthenticationResult.AccessToken), + nameof(AuthenticationResult.UniqueId), + nameof(AuthenticationResult.ExpiresOn), + nameof(AuthenticationResult.TenantId), + nameof(AuthenticationResult.Account), + nameof(AuthenticationResult.IdToken), + nameof(AuthenticationResult.Scopes), + nameof(AuthenticationResult.CorrelationId), + nameof(AuthenticationResult.TokenType), + + // SPA / mTLS extras + nameof(AuthenticationResult.SpaAuthCode), + nameof(AuthenticationResult.BindingCertificate), + + // ancillary data + nameof(AuthenticationResult.AdditionalResponseParameters), + nameof(AuthenticationResult.ClaimsPrincipal), + nameof(AuthenticationResult.AuthenticationResultMetadata) + ]; + + // ---- reflection gather ---- + var propsWithPublicSetter = typeof(AuthenticationResult) + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.GetCustomAttribute() == null) // skip obsolete + .Where(p => p.GetSetMethod(/*nonPublic*/ false) != null) // has public setter + .Select(p => p.Name) + .OrderBy(n => n) + .ToArray(); + + // ---- assertion ---- + CollectionAssert.AreEquivalent( + expected.OrderBy(n => n).ToArray(), + propsWithPublicSetter, + "All non-obsolete public properties should expose a public setter." + ); + } } } diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs index 40e55b1492..7ed05927a0 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/MtlsPopTests.cs @@ -263,6 +263,10 @@ public async Task AcquireTokenForClient_WithMtlsProofOfPossession_SuccessAsync() Assert.AreEqual(region, result.AuthenticationResultMetadata.RegionDetails.RegionUsed); Assert.AreEqual(expectedTokenEndpoint, result.AuthenticationResultMetadata.TokenEndpoint); + Assert.IsNotNull(result.BindingCertificate, "BindingCertificate should be present."); + Assert.AreEqual(s_testCertificate.Thumbprint, result.BindingCertificate.Thumbprint, + "BindingCertificate must match the cert passed to WithCertificate()."); + // Second token acquisition - should retrieve from cache AuthenticationResult secondResult = await app.AcquireTokenForClient(TestConstants.s_scope) .WithMtlsProofOfPossession() @@ -273,6 +277,10 @@ public async Task AcquireTokenForClient_WithMtlsProofOfPossession_SuccessAsync() Assert.AreEqual(Constants.MtlsPoPAuthHeaderPrefix, secondResult.TokenType); Assert.AreEqual(TokenSource.Cache, secondResult.AuthenticationResultMetadata.TokenSource); Assert.AreEqual(expectedTokenEndpoint, result.AuthenticationResultMetadata.TokenEndpoint); + // Cached result must still carry the cert + Assert.IsNotNull(secondResult.BindingCertificate); + Assert.AreEqual(result.BindingCertificate.Thumbprint, + secondResult.BindingCertificate.Thumbprint); } } } @@ -765,5 +773,50 @@ public async Task MtlsPop_WithUnsupportedNonTenantedAuthorityAsyncForDsts_Throws .ExecuteAsync()) .ConfigureAwait(false); } + + [TestMethod] + public async Task BindingCertificate_PopulatedForMtlsPop_AndNullForBearerAsync() + { + const string region = "eastus"; + using var env = new EnvVariableContext(); + Environment.SetEnvironmentVariable("REGION_NAME", region); + + using var httpManager = new MockHttpManager(); + { + // Token call for MTLS-PoP + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage( + tokenType: "mtls_pop"); + // Token call for bearer – second AcquireToken uses this + httpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); // defaults to Bearer + + var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) + .WithCertificate(s_testCertificate) + .WithAuthority("https://login.microsoftonline.com/123456-1234-2345-1234561234") + .WithAzureRegion(ConfidentialClientApplication.AttemptRegionDiscovery) + .WithExperimentalFeatures() + .WithHttpManager(httpManager) + .BuildConcrete(); + + // -------- 1st call: MTLS-PoP -------- + AuthenticationResult popResult = await app.AcquireTokenForClient(TestConstants.s_scope) + .WithMtlsProofOfPossession() + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual(Constants.MtlsPoPAuthHeaderPrefix, popResult.TokenType); + Assert.IsNotNull(popResult.BindingCertificate, "BindingCertificate should be set for MTLS-PoP."); + Assert.AreEqual(s_testCertificate.Thumbprint, + popResult.BindingCertificate.Thumbprint, + "BindingCertificate thumbprint should match the cert supplied via WithCertificate()."); + + // -------- 2nd call: Bearer -------- + AuthenticationResult bearerResult = await app.AcquireTokenForClient(TestConstants.s_scope) + .ExecuteAsync() + .ConfigureAwait(false); + + Assert.AreEqual("Bearer", bearerResult.TokenType); + Assert.IsNull(bearerResult.BindingCertificate, "BindingCertificate must be null for Bearer tokens."); + } + } } }