diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index ef38621..763c7fc 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -28,6 +28,14 @@ jobs: - name: Build and test run: dotnet test -p:TargetNetNext=${{ env.TargetNetNext }} -v m --configuration Release Microsoft.Identity.Abstractions.sln + # Run AOT tests to validate no reflection warnings (requires .NET 10) + - name: Publish and run AOT tests + run: | + cd test/Microsoft.Identity.Abstractions.AotTests + dotnet publish --runtime win-x64 -f net10.0 --configuration Release -v m + .\bin\Release\net10.0\win-x64\publish\Microsoft.Identity.Abstractions.AotTests.exe + shell: pwsh + # Pack the library - name: Pack run: dotnet pack -p:TargetNetNext=${{ env.TargetNetNext }} --configuration Release --no-restore --no-build -v m Microsoft.Identity.Abstractions.sln diff --git a/Directory.Build.props b/Directory.Build.props index 2dfe587..8cf6068 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 10.0.0 + 11.0.0 $(MicrosoftIdentityAbstractionsVersion) $(MSBuildThisFileDirectory)\build\35MSSharedLib1024.snk @@ -39,7 +39,9 @@ ../../build/MSAL.snk true enable - 12 + + 14 true 8.0.0 4.14.0 diff --git a/Microsoft.Identity.Abstractions.sln b/Microsoft.Identity.Abstractions.sln index 5be6142..9786b25 100644 --- a/Microsoft.Identity.Abstractions.sln +++ b/Microsoft.Identity.Abstractions.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.11205.157 d18.0 +VisualStudioVersion = 18.0.11205.157 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Abstractions", "src\Microsoft.Identity.Abstractions\Microsoft.Identity.Abstractions.csproj", "{98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}" EndProject @@ -25,20 +25,54 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D5BF0954-2 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Abstractions.Tests", "test\Microsoft.Identity.Abstractions.Tests\Microsoft.Identity.Abstractions.Tests.csproj", "{AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Abstractions.AotTests", "test\Microsoft.Identity.Abstractions.AotTests\Microsoft.Identity.Abstractions.AotTests.csproj", "{39865C17-1E90-495D-BCB1-C171FCE7078D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|x64.ActiveCfg = Debug|Any CPU + {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|x64.Build.0 = Debug|Any CPU + {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|x86.ActiveCfg = Debug|Any CPU + {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Debug|x86.Build.0 = Debug|Any CPU {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|Any CPU.Build.0 = Release|Any CPU + {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|x64.ActiveCfg = Release|Any CPU + {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|x64.Build.0 = Release|Any CPU + {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|x86.ActiveCfg = Release|Any CPU + {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F}.Release|x86.Build.0 = Release|Any CPU {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|x64.ActiveCfg = Debug|Any CPU + {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|x64.Build.0 = Debug|Any CPU + {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|x86.ActiveCfg = Debug|Any CPU + {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Debug|x86.Build.0 = Debug|Any CPU {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|Any CPU.ActiveCfg = Release|Any CPU {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|Any CPU.Build.0 = Release|Any CPU + {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|x64.ActiveCfg = Release|Any CPU + {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|x64.Build.0 = Release|Any CPU + {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|x86.ActiveCfg = Release|Any CPU + {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2}.Release|x86.Build.0 = Release|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|x64.ActiveCfg = Debug|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|x64.Build.0 = Debug|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|x86.ActiveCfg = Debug|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Debug|x86.Build.0 = Debug|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|Any CPU.Build.0 = Release|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|x64.ActiveCfg = Release|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|x64.Build.0 = Release|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|x86.ActiveCfg = Release|Any CPU + {39865C17-1E90-495D-BCB1-C171FCE7078D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -46,6 +80,7 @@ Global GlobalSection(NestedProjects) = preSolution {98F57CC8-01A0-49F3-B859-DDC4F8F5CD2F} = {68824A9C-D009-491A-8A76-680A261A8C71} {AC403F8B-5ADB-4037-A62A-73FEC3E7B0E2} = {D5BF0954-25F6-40ED-9896-8EDB99EBEF5A} + {39865C17-1E90-495D-BCB1-C171FCE7078D} = {D5BF0954-25F6-40ED-9896-8EDB99EBEF5A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {34D01AD8-BCE0-44B3-9197-439FF4A1D46D} diff --git a/changelog.md b/changelog.md index 93065e4..71d61f5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,30 @@ +11.0.0 +======= +## Breaking changes + +If you build code with the .NET 10 target framework and get this error: +```txt +error CS9260: Feature 'extensions' is not available in C# 13.0. Please use language version 14.0 or greater. +``` +make sure you update the `LangVersion` property in your project to 14 or later. + +### AOT/NativeAOT Compatibility for .NET 10+ + +Made `CredentialDescription` AOT-compatible for .NET 10+ by using C# 14 extension properties. This is a **binary breaking change** (though source compatible) for .NET 10+ targets: +- Removes `Certificate` and `CachedValue` as public properties from `CredentialDescription` when targeting .NET 10+ +- Adds extension properties with the same names and signatures for .NET 10+, providing property-style access +- Maintains full source compatibility - no code changes required for consumers provided the .NET 10 code is built with C#14 or later. +- Prevents AOT/NativeAOT configuration binding issues with reference-typed properties +- Keeps existing behavior for older target frameworks (netstandard2.0, netstandard2.1, net462, net8.0, net9.0) + +**Technical details:** +- For .NET 10+: `Certificate` and `CachedValue` are implemented as extension properties (not visible to config binders) +- For older TFMs: `Certificate` and `CachedValue` remain as regular public properties +- LangVersion updated to `14` to enable C# 14 extension property syntax +- Internal accessor methods (`GetCertificateInternal`, `SetCertificateInternal`, etc.) support extension properties + +This enhancement ensures `CredentialDescription` works seamlessly in AOT/NativeAOT compilation scenarios while maintaining backward compatibility. + 10.0.0 ======= ## Breaking changes @@ -5,6 +32,8 @@ Rename `IAuthorizationHeaderProvider2` to `BoundAuthorizationHeaderProvider`. Th In practice, it's unlikely that this breaking change affects anybody as the renamed interface was new in 9.6.0, and not yet used to the team's knowledge. +## Improvements and fundamentals + 9.6.0 ====== ## New features diff --git a/src/Microsoft.Identity.Abstractions/ApplicationOptions/CredentialDescription.cs b/src/Microsoft.Identity.Abstractions/ApplicationOptions/CredentialDescription.cs index b699aae..09106ee 100644 --- a/src/Microsoft.Identity.Abstractions/ApplicationOptions/CredentialDescription.cs +++ b/src/Microsoft.Identity.Abstractions/ApplicationOptions/CredentialDescription.cs @@ -42,8 +42,9 @@ public CredentialDescription(CredentialDescription other) #endif Algorithm = other.Algorithm; Base64EncodedValue = other.Base64EncodedValue; - CachedValue = other.CachedValue; - Certificate = other.Certificate; + // Copy the backing fields directly. This is safe to do in a copy-constructor, because the ID is NULL initially. + _cachedValue = other._cachedValue; + _certificate = other._certificate; CertificateStorePath = other.CertificateStorePath; CertificateDistinguishedName = other.CertificateDistinguishedName; CertificateThumbprint = other.CertificateThumbprint; @@ -77,14 +78,15 @@ public string Id { if (_cachedId == null) { - string certificateThumbprint = Certificate?.Thumbprint ?? "null"; + // Use backing field directly to work in both .NET 10+ and older TFMs + string certificateThumbprint = _certificate?.Thumbprint ?? "null"; switch (SourceType) { case CredentialSource.Certificate: - if (Certificate != null) + if (_certificate != null) { - _cachedId = $"Certificate={Certificate.Thumbprint}"; + _cachedId = $"Certificate={_certificate.Thumbprint}"; } else { @@ -126,9 +128,9 @@ public string Id _cachedId = $"CustomSignedAssertion={CustomSignedAssertionProviderName}({parameterNames})"; break; case CredentialSource.ManagedCertificate: - if (CachedValue != null) + if (_cachedValue != null) { - _cachedId = $"ManagedCertificate={CachedValue};Thumbprint={certificateThumbprint}"; + _cachedId = $"ManagedCertificate={_cachedValue};Thumbprint={certificateThumbprint}"; } else { @@ -417,6 +419,49 @@ public string? ClientSecret /// If you want to use the default authority, don't provide a token exchange authority URL. public string? TokenExchangeAuthority { get; set; } +#if NET10_0_OR_GREATER + // For .NET 10+, use protected internal methods to avoid AOT issues with configuration binders + // Extension properties (defined in CredentialDescriptionExtensions.cs) provide property-style access + // Methods are protected internal to allow derived classes to access them + + /// + /// Gets the certificate. For .NET 10+, use the Certificate extension property instead. + /// This method is protected internal to allow derived classes to access the certificate. + /// +#pragma warning disable CA1024 // Use properties where appropriate + protected internal X509Certificate2? GetCertificateInternal() => _certificate; +#pragma warning restore CA1024 // Use properties where appropriate + + /// + /// Sets the certificate. For .NET 10+, use the Certificate extension property instead. + /// This method is protected internal to allow derived classes to set the certificate. + /// + protected internal void SetCertificateInternal(X509Certificate2? value) + { + _certificate = value; + // Cached Id can depend on the certificate thumbprint. Set it to null so that it will be recomputed. + _cachedId = null; + } + + /// + /// Gets the cached value. For .NET 10+, use the CachedValue extension property instead. + /// This method is protected internal to allow derived classes to access the cached value. + /// +#pragma warning disable CA1024 // Use properties where appropriate + protected internal object? GetCachedValueInternal() => _cachedValue; +#pragma warning restore CA1024 // Use properties where appropriate + + /// + /// Sets the cached value. For .NET 10+, use the CachedValue extension property instead. + /// This method is protected internal to allow derived classes to set the cached value. + /// + protected internal void SetCachedValueInternal(object? value) + { + _cachedValue = value; + // Cached Id can depend on the cached value. Set it to null so that it will be recomputed. + _cachedId = null; + } +#else /// /// When is , you will use this property to provide the certificate yourself. /// When is or @@ -450,6 +495,7 @@ public virtual object? CachedValue _cachedId = null; } } +#endif /// /// Skip this credential description. This is useful when, you specify a list of diff --git a/src/Microsoft.Identity.Abstractions/ApplicationOptions/CredentialDescriptionExtensions.cs b/src/Microsoft.Identity.Abstractions/ApplicationOptions/CredentialDescriptionExtensions.cs new file mode 100644 index 0000000..9526e31 --- /dev/null +++ b/src/Microsoft.Identity.Abstractions/ApplicationOptions/CredentialDescriptionExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#if NET10_0_OR_GREATER +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.Identity.Abstractions +{ + /// + /// Provides extension properties for instances (.NET 10+ only). + /// These extension properties provide property-style access to Certificate and CachedValue + /// while keeping them hidden from AOT/NativeAOT configuration binders. + /// + /// + /// This uses C# 14 extension property syntax. While C# 14 extension properties as a language feature + /// are available on any target framework, for this library they are only exposed on .NET 10+ by design + /// to avoid binary breaking changes on older frameworks. + /// The extension block defines properties that appear as instance properties on CredentialDescription + /// but are not part of the type's public API surface visible to reflection-based tools like configuration binders. + /// + public static class CredentialDescriptionExtensions + { +#pragma warning disable CA1034 // Do not nest type - this is intentional C# 14 extension block syntax + // C# 14 extension block syntax - defines extension properties on CredentialDescription + extension(CredentialDescription credential) + { + /// + /// When is , you will use this property to provide the certificate yourself. + /// When is or + /// or or or + /// after the certificate is retrieved by a , it will be stored in this property and also in the CachedValue property. + /// + public X509Certificate2? Certificate + { + get => credential.GetCertificateInternal(); + set => credential.SetCertificateInternal(value); + } + + /// + /// When the credential is retrieved by a , it will be stored in this property, where you can retrieve it. If the credential is a certificate, + /// it will also be stored in the Certificate property. + /// + public object? CachedValue + { + get => credential.GetCachedValueInternal(); + set => credential.SetCachedValueInternal(value); + } + } +#pragma warning restore CA1034 + } +} +#endif diff --git a/src/Microsoft.Identity.Abstractions/ApplicationOptions/CredentialSource.cs b/src/Microsoft.Identity.Abstractions/ApplicationOptions/CredentialSource.cs index f07e7c0..52d50c7 100644 --- a/src/Microsoft.Identity.Abstractions/ApplicationOptions/CredentialSource.cs +++ b/src/Microsoft.Identity.Abstractions/ApplicationOptions/CredentialSource.cs @@ -15,7 +15,7 @@ public enum CredentialSource { /// /// Use this value if you provide a certificate yourself. When setting the property to this value, - /// you will also provide the . + /// you will also provide the CredentialDescription.Certificate. /// Certificate = 0, diff --git a/src/Microsoft.Identity.Abstractions/ApplicationOptions/ICredentialsLoader.cs b/src/Microsoft.Identity.Abstractions/ApplicationOptions/ICredentialsLoader.cs index bb63280..2354a43 100644 --- a/src/Microsoft.Identity.Abstractions/ApplicationOptions/ICredentialsLoader.cs +++ b/src/Microsoft.Identity.Abstractions/ApplicationOptions/ICredentialsLoader.cs @@ -9,7 +9,7 @@ namespace Microsoft.Identity.Abstractions /// /// Contract for credential loaders, implemented by classes like the DefaultCertificateLoader or the DefaultCredentialLoader /// in Microsoft.Identity.Web. Credential loaders are used to load credentials from a , the result - /// is then in the property. + /// is then in the CredentialDescription.CachedValue property. /// Credential loaders constitute an extensibility point. They delegate to credential source loaders, which are specified in the /// collection, choosing the one which matches the credential source of the /// credential description to load. diff --git a/src/Microsoft.Identity.Abstractions/CompatibilitySuppressions.xml b/src/Microsoft.Identity.Abstractions/CompatibilitySuppressions.xml index 17afab9..65b2998 100644 --- a/src/Microsoft.Identity.Abstractions/CompatibilitySuppressions.xml +++ b/src/Microsoft.Identity.Abstractions/CompatibilitySuppressions.xml @@ -1,6 +1,30 @@  + + CP0002 + M:Microsoft.Identity.Abstractions.CredentialDescription.get_CachedValue + lib/net9.0/Microsoft.Identity.Abstractions.dll + lib/net10.0/Microsoft.Identity.Abstractions.dll + + + CP0002 + M:Microsoft.Identity.Abstractions.CredentialDescription.get_Certificate + lib/net9.0/Microsoft.Identity.Abstractions.dll + lib/net10.0/Microsoft.Identity.Abstractions.dll + + + CP0002 + M:Microsoft.Identity.Abstractions.CredentialDescription.set_CachedValue(System.Object) + lib/net9.0/Microsoft.Identity.Abstractions.dll + lib/net10.0/Microsoft.Identity.Abstractions.dll + + + CP0002 + M:Microsoft.Identity.Abstractions.CredentialDescription.set_Certificate(System.Security.Cryptography.X509Certificates.X509Certificate2) + lib/net9.0/Microsoft.Identity.Abstractions.dll + lib/net10.0/Microsoft.Identity.Abstractions.dll + CP0006 M:Microsoft.Identity.Abstractions.IDownstreamApi.PatchForAppAsync``1(System.String,``0,System.Action{Microsoft.Identity.Abstractions.DownstreamApiOptionsReadOnlyHttpMethod},System.Threading.CancellationToken) diff --git a/src/Microsoft.Identity.Abstractions/PublicAPI/net10.0/PublicAPI.Shipped.txt b/src/Microsoft.Identity.Abstractions/PublicAPI/net10.0/PublicAPI.Shipped.txt index 2670b54..df8f089 100644 --- a/src/Microsoft.Identity.Abstractions/PublicAPI/net10.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.Identity.Abstractions/PublicAPI/net10.0/PublicAPI.Shipped.txt @@ -102,8 +102,6 @@ Microsoft.Identity.Abstractions.CredentialDescription.Algorithm.get -> string? Microsoft.Identity.Abstractions.CredentialDescription.Algorithm.set -> void Microsoft.Identity.Abstractions.CredentialDescription.Base64EncodedValue.get -> string? Microsoft.Identity.Abstractions.CredentialDescription.Base64EncodedValue.set -> void -Microsoft.Identity.Abstractions.CredentialDescription.Certificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2? -Microsoft.Identity.Abstractions.CredentialDescription.Certificate.set -> void Microsoft.Identity.Abstractions.CredentialDescription.CertificateDiskPath.get -> string? Microsoft.Identity.Abstractions.CredentialDescription.CertificateDiskPath.set -> void Microsoft.Identity.Abstractions.CredentialDescription.CertificateDistinguishedName.get -> string? @@ -349,7 +347,5 @@ static Microsoft.Identity.Abstractions.OperationResult.implicit static Microsoft.Identity.Abstractions.OperationResult.implicit operator Microsoft.Identity.Abstractions.OperationResult(TResult result) -> Microsoft.Identity.Abstractions.OperationResult virtual Microsoft.Identity.Abstractions.AcquireTokenOptions.Clone() -> Microsoft.Identity.Abstractions.AcquireTokenOptions! virtual Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions.CloneInternal() -> Microsoft.Identity.Abstractions.AuthorizationHeaderProviderOptions! -virtual Microsoft.Identity.Abstractions.CredentialDescription.CachedValue.get -> object? -virtual Microsoft.Identity.Abstractions.CredentialDescription.CachedValue.set -> void virtual Microsoft.Identity.Abstractions.IdentityApplicationOptions.Authority.get -> string? virtual Microsoft.Identity.Abstractions.IdentityApplicationOptions.Authority.set -> void diff --git a/src/Microsoft.Identity.Abstractions/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Microsoft.Identity.Abstractions/PublicAPI/net10.0/PublicAPI.Unshipped.txt index e46d2cb..03f0cea 100644 --- a/src/Microsoft.Identity.Abstractions/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.Identity.Abstractions/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1,3 +1,21 @@ #nullable enable +*REMOVED*Microsoft.Identity.Abstractions.CredentialDescription.Certificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2? +*REMOVED*Microsoft.Identity.Abstractions.CredentialDescription.Certificate.set -> void +*REMOVED*virtual Microsoft.Identity.Abstractions.CredentialDescription.CachedValue.get -> object? +*REMOVED*virtual Microsoft.Identity.Abstractions.CredentialDescription.CachedValue.set -> void +Microsoft.Identity.Abstractions.CredentialDescription.GetCachedValueInternal() -> object? +Microsoft.Identity.Abstractions.CredentialDescription.GetCertificateInternal() -> System.Security.Cryptography.X509Certificates.X509Certificate2? +Microsoft.Identity.Abstractions.CredentialDescription.SetCachedValueInternal(object? value) -> void +Microsoft.Identity.Abstractions.CredentialDescription.SetCertificateInternal(System.Security.Cryptography.X509Certificates.X509Certificate2? value) -> void +Microsoft.Identity.Abstractions.CredentialDescriptionExtensions +Microsoft.Identity.Abstractions.CredentialDescriptionExtensions.extension(Microsoft.Identity.Abstractions.CredentialDescription!) +Microsoft.Identity.Abstractions.CredentialDescriptionExtensions.extension(Microsoft.Identity.Abstractions.CredentialDescription!).CachedValue.get -> object? +Microsoft.Identity.Abstractions.CredentialDescriptionExtensions.extension(Microsoft.Identity.Abstractions.CredentialDescription!).CachedValue.set -> void +Microsoft.Identity.Abstractions.CredentialDescriptionExtensions.extension(Microsoft.Identity.Abstractions.CredentialDescription!).Certificate.get -> System.Security.Cryptography.X509Certificates.X509Certificate2? +Microsoft.Identity.Abstractions.CredentialDescriptionExtensions.extension(Microsoft.Identity.Abstractions.CredentialDescription!).Certificate.set -> void +static Microsoft.Identity.Abstractions.CredentialDescriptionExtensions.get_CachedValue(Microsoft.Identity.Abstractions.CredentialDescription! credential) -> object? +static Microsoft.Identity.Abstractions.CredentialDescriptionExtensions.get_Certificate(Microsoft.Identity.Abstractions.CredentialDescription! credential) -> System.Security.Cryptography.X509Certificates.X509Certificate2? +static Microsoft.Identity.Abstractions.CredentialDescriptionExtensions.set_CachedValue(Microsoft.Identity.Abstractions.CredentialDescription! credential, object? value) -> void +static Microsoft.Identity.Abstractions.CredentialDescriptionExtensions.set_Certificate(Microsoft.Identity.Abstractions.CredentialDescription! credential, System.Security.Cryptography.X509Certificates.X509Certificate2? value) -> void Microsoft.Identity.Abstractions.IBoundAuthorizationHeaderProvider Microsoft.Identity.Abstractions.IBoundAuthorizationHeaderProvider.CreateBoundAuthorizationHeaderAsync(Microsoft.Identity.Abstractions.DownstreamApiOptions! downstreamApiOptions, System.Security.Claims.ClaimsPrincipal? claimsPrincipal = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task>! diff --git a/test/Microsoft.Identity.Abstractions.AotTests/Microsoft.Identity.Abstractions.AotTests.csproj b/test/Microsoft.Identity.Abstractions.AotTests/Microsoft.Identity.Abstractions.AotTests.csproj new file mode 100644 index 0000000..e3c5683 --- /dev/null +++ b/test/Microsoft.Identity.Abstractions.AotTests/Microsoft.Identity.Abstractions.AotTests.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + $(TargetFrameworks); + enable + enable + false + exe + true + Microsoft.Identity.Abstractions.AotTests.Program + true + + + + + Always + + + + + + + + + + + + + diff --git a/test/Microsoft.Identity.Abstractions.AotTests/Program.cs b/test/Microsoft.Identity.Abstractions.AotTests/Program.cs new file mode 100644 index 0000000..21f89ba --- /dev/null +++ b/test/Microsoft.Identity.Abstractions.AotTests/Program.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Identity.Abstractions; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.Identity.Abstractions.AotTests; + +class Program +{ + private const string ApiName = "Api1"; + + static void Main() + { + // For each change, you need to run the following + // run `dotnet publish --runtime win-x64 -f net10.0` + // and then `.\bin\Release\net10.0\win-x64\publish\Microsoft.Identity.Abstractions.AotTests.exe` + IHostBuilder hostBuilder = Host.CreateDefaultBuilder() + .ConfigureServices((context, services) => + { + var azureAdSection = context.Configuration.GetSection("AzureAd"); + services.Configure("scheme", options => azureAdSection.Bind(options)); + services.Configure(ApiName, + options => context.Configuration.GetSection($"DownstreamApis:{ApiName}").Bind(options) + ); + }); + + + IHost host = hostBuilder.Build(); + + IOptionsMonitor options = host.Services.GetRequiredService>(); + + MicrosoftIdentityApplicationOptions optionsInstance = options.Get("scheme"); + + if (string.IsNullOrEmpty(optionsInstance?.ClientId)) + { + throw new InvalidOperationException("could not bind client id"); + } + else + { + Console.WriteLine($"ClientId: {optionsInstance.ClientId}"); + Console.WriteLine($"ClientCredentials: {optionsInstance.ClientCredentials!.First().Id}"); + } + + IOptionsMonitor downstreamApisOptions = host.Services.GetRequiredService>(); + var option = downstreamApisOptions.Get(ApiName); + Console.WriteLine($"DownstreamApi: {option.BaseUrl}"); + + Console.WriteLine("AOT test completed successfully!"); + } +} diff --git a/test/Microsoft.Identity.Abstractions.AotTests/Publish.bat b/test/Microsoft.Identity.Abstractions.AotTests/Publish.bat new file mode 100644 index 0000000..a405acf --- /dev/null +++ b/test/Microsoft.Identity.Abstractions.AotTests/Publish.bat @@ -0,0 +1,4 @@ +REM Needs to be published to make sure we don't have any AoT warnings + +dotnet publish --runtime win-x64 -f net10.0 +bin\Release\net10.0\win-x64\publish\Microsoft.Identity.Abstractions.AotTests.exe diff --git a/test/Microsoft.Identity.Abstractions.AotTests/README.md b/test/Microsoft.Identity.Abstractions.AotTests/README.md new file mode 100644 index 0000000..4ef4553 --- /dev/null +++ b/test/Microsoft.Identity.Abstractions.AotTests/README.md @@ -0,0 +1,72 @@ +# Microsoft.Identity.Abstractions AOT Tests + +This project tests AOT (Ahead-of-Time) compilation compatibility for Microsoft.Identity.Abstractions, specifically for configuration binding scenarios on .NET 10+. + +## Purpose + +The AOT test project verifies that: +1. `CredentialDescription` and related types can be bound from configuration without reflection +2. Extension properties work correctly with configuration binders +3. No AOT warnings are generated during publish + +## Running the Tests + +### On Windows + +```batch +cd test/Microsoft.Identity.Abstractions.AotTests +Publish.bat +``` + +### On Linux/macOS + +```bash +cd test/Microsoft.Identity.Abstractions.AotTests +dotnet publish --runtime linux-x64 -f net10.0 +./bin/Release/net10.0/linux-x64/publish/Microsoft.Identity.Abstractions.AotTests +``` + +### Manual Publish + +```bash +dotnet publish --runtime -f net10.0 +``` + +Where `` is one of: +- `win-x64` (Windows) +- `linux-x64` (Linux) +- `osx-x64` (macOS Intel) +- `osx-arm64` (macOS Apple Silicon) + +## What It Tests + +The test application: +1. Loads configuration from `appsettings.json` +2. Binds `MicrosoftIdentityApplicationOptions` with `CredentialDescription` objects +3. Binds `DownstreamApisOptions` +4. Verifies all properties are correctly bound +5. Outputs success messages if binding worked + +## Expected Output + +``` +ClientId: 6c4e1e3e-9b3e-4b3e-8e3e-1e3e9b3e4b3e +ClientCredentials: CertificateFromKeyVault=https://mykeyvault.vault.azure.net/myCertName;Thumbprint=null +DownstreamApis count: 2 +DownstreamApi: Api1 +DownstreamApi: https://api1.com +DownstreamApi: Api2 +DownstreamApi: https://api2.com +AOT test completed successfully! +``` + +## Key Features + +- Uses `PublishAot=true` to enable AOT compilation +- Uses `EnableConfigurationBindingGenerator=true` for source-generated configuration binding +- Tests that Certificate and CachedValue properties (implemented as extension properties on .NET 10+) work with configuration binding +- Verifies no reflection warnings during publish + +## Related + +This test project validates the AOT compatibility changes made in version 11.0.0 where `Certificate` and `CachedValue` were converted to C# 14 extension properties to hide them from configuration binders. diff --git a/test/Microsoft.Identity.Abstractions.AotTests/appsettings.json b/test/Microsoft.Identity.Abstractions.AotTests/appsettings.json new file mode 100644 index 0000000..bfc66ae --- /dev/null +++ b/test/Microsoft.Identity.Abstractions.AotTests/appsettings.json @@ -0,0 +1,32 @@ +{ + "AzureAd": { + "Name": "Bearer", + "Instance": "https://login.microsoftonline.com/", + "TenantId": "common", + "AppHomeTenantId": "f8cdef31-a31e-4b4a-93e4-5f571e91255a", + "AzureRegion": "TryAutoDetect", + "ClientCapabilities": [ "cp1" ], + "SendX5C": true, + "ClientId": "6c4e1e3e-9b3e-4b3e-8e3e-1e3e9b3e4b3e", + "EnablePiiLogging": true, + "ExtraQueryParameters": { + "Test": "TestSlice" + }, + "ClientCredentials": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://mykeyvault.vault.azure.net", + "KeyVaultCertificateName": "myCertName" + } + ], + "Audiences": [ "https://audience.microsoft.com" ], + "TokenDecryptionCredentials": [] + }, + + "DownstreamApis": { + "Api1": { + "BaseUrl": "https://api1.com", + "Scopes": [ "api1.read" ] + } + } +} diff --git a/test/Microsoft.Identity.Abstractions.Tests/Microsoft.Identity.Abstractions.Tests.csproj b/test/Microsoft.Identity.Abstractions.Tests/Microsoft.Identity.Abstractions.Tests.csproj index dd4d1ac..f4c55ea 100644 --- a/test/Microsoft.Identity.Abstractions.Tests/Microsoft.Identity.Abstractions.Tests.csproj +++ b/test/Microsoft.Identity.Abstractions.Tests/Microsoft.Identity.Abstractions.Tests.csproj @@ -5,7 +5,7 @@ $(TargetFrameworks); enable false - 12 + 14 True ../../build/MSAL.snk