Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose credential loader extensibility to support custom signed assertion providers #3220

Open
7 tasks
jmprieur opened this issue Jan 24, 2025 · 0 comments · May be fixed by #3226
Open
7 tasks

Expose credential loader extensibility to support custom signed assertion providers #3220

jmprieur opened this issue Jan 24, 2025 · 0 comments · May be fixed by #3226
Assignees

Comments

@jmprieur
Copy link
Collaborator

jmprieur commented Jan 24, 2025

Summary

This design proposal aims at adding extensibility to DefaultCredentialsLoader so that partner teams, or SDK on top of Microsoft.Identity.Web can bring their own credential providers.

See also AzureAD/microsoft-identity-abstractions-for-dotnet#146 and AzureAD/microsoft-identity-abstractions-for-dotnet#153 which are pre-requisites

Motivation and goals

See Add extensibility to CredentialDescription to support custom signed assertion providers for the devex for users of the extensibility.

In that article, we mentioned that the developer using this extensibility provides CredentialDescription with a specific CustomSignedAssertionProviderName and calls an extension method on the service collection (for instance services.AddFmiProvider();). This design proposal explains how the extender provides

  • the extension method on the service collection.
  • the signed assertion credential source loader, and the signed assertion provider
  • the value that developers need to use in CustomSignedAssertionProviderName (possibly the data),

In scope

  • This design proposal is for the DevEx for the provider of the extension (that is the extender)

Out of scope

Proposed developer experience for the extender providing a custom assertion provider

Remember, the developer experience for the developer using the extension is described in Add extensibility to CredentialDescription to support custom signed assertion providers

Configuration file

Nothing to do. This is not about the developer using the extensibility

Startup.cs

Nothing to do. This is not about the developer using the extensibility

Classes provided as the extension

The developer providing the extension needs to provide three classes:

  1. A class deriving from ClientAssertionProviderBase and that will compute or retrieve the signed assertion.

    Exemple:

       internal class MyCustomSignedAssertionProvider : ClientAssertionProviderBase
       {
           public MyCustomSignedAssertionProvider(Dictionary<string, object>? properties)
           {
               // Here you would implement the logic to extract what you need from the properties passed in
               // the configuration
           }
           protected override Task<ClientAssertion> GetClientAssertionAsync(AssertionRequestOptions? assertionRequestOptions)
           {
               // Here you would implement the logic to get the signed assertion, which is probably going
               // to be a call to a service. This call can be parameterized by the parameters in the properties
               // of the constructor.
    
               // In this sample code we just create an empty signed assertion and return it.
               var clientAssertion = new ClientAssertion("FakeAssertion", DateTimeOffset.Now);
               return Task.FromResult(clientAssertion);
           }
       }
  2. Provide a class implementing ICustomSignedAssertionProvider which needs to:

    • get from the cached value or instantiate the class in 1) (MyCustomSignedAssertionProvider ) in
      the LoadIfNeededAsync overload
    • Call GetSignedAssertionAsync
    • process the exception and set the Skip flag. Set the CachedValue to the new MyCustomSignedAssertionProvider in case there is no exception.
    namespace ExtensibilityTests
    {
        internal class MyCustomSignedAssertionLoader : ICustomSignedAssertionProvider
        {
            public MyCustomSignedAssertionLoader(ILogger<DefaultCredentialsLoader> logger)
            {
                _logger = logger;
            }
            public CredentialSource CredentialSource => CredentialSource.CustomSignedAssertion;
    
            public string Name => "MyCustomExtension";
    
            private ILogger<DefaultCredentialsLoader> _logger;
    
            public async Task LoadIfNeededAsync(CredentialDescription credentialDescription, CredentialSourceLoaderParameters? parameters = null)
            {
                MyCustomSignedAssertionProvider? signedAssertion = credentialDescription.CachedValue as MyCustomSignedAssertionProvider;
                if (credentialDescription.CachedValue == null)
                {
                    signedAssertion = new MyCustomSignedAssertionProvider(credentialDescription.CustomSignedAssertionProviderData);
                }
                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!.GetSignedAssertionAsync(null);
                    credentialDescription.CachedValue = signedAssertion;
                }
                catch (Exception)
                {
                    credentialDescription.Skip = true;
                    throw;
                }
            }
        }
    }
  3. Write an extension method to register the extension with the service collection. This is the only class the developer
    using the extension will need to use.

       public static IServiceCollection AddCustomSignedAssertionProvider(
          this IServiceCollection services)
      {
          services.AddSingleton<ICustomSignedAssertionProvider, MyCustomSignedAssertionLoader>();
          return services;
      }

Design/Work to be done in Microsoft.Identity.Web

  1. In Microsoft.Identity.Web.DefaultCredentialsLoader (in the Microsoft.Identity.Web.Certificate)

    • Add a public property CustomSignedAssertionCredentialSourceLoaders of type
      IDictionary<string, ICustomSignedAssertionProvider>

        /// <summary>
        /// Dictionary of custom signed assertion credential source loaders, by name (fully qualified type name).
        /// </summary>
        public IDictionary<string, ICredentialSourceLoader> CustomSignedAssertionCredentialSourceLoaders { get; }
    • Add an extra parameter to the constructor of DefaultCredentialLoader: signedAssertionProviders
      of type IEnumerable<ICustomSignedAssertionProvider>? with a default value of null, to avoid a breaking change.

    • Initialize CustomSignedAssertionCredentialSourceLoaders it in the constructor of DefaultCredentialsLoader based
      on the values in signedAssertionProviders. For each provider, the key would be provider.Name ?? provider.GetType().FullName!

    • In the LoadCredentialsIfNeededAsync method, when the CachedValue is null, if the sourceType of the credential
      description is CredentialSource.CustomSignedAssertion and credentialDescription.CustomSignedAssertionProviderName
      is defined, instead of using the CredentialsSourceLoaders, use the CustomSignedAssertionSourceLoaders. The code will
      be very similar in both cases (custom providers or in the box providers).
      If the CustomSignedAssertionCredentialSourceLoaders of name
      credentialDescription.CustomSignedAssertionProviderName is not found, we need to log a new error
      (CustomSignedAssertionProviderNotFound):

       "Failed to find custom signed assertion provider {name} from source {sourceType}. Will it be skipped in the future ? {skip}.");
      
    • Modify the DefaultCredentialLoader.Logger.cs file to add the new log message.

  2. In Microsoft.Identity.Web.DefautCertificateLoader

    • Add the same kind of constructor in DefaultCertificateLoader (calling the constructor of the base class). This will
      be used by the Dependency injection to inject the ICustomSignedAssertionProvider (custom providers)
  3. In ConfidentialClientApplicationBuilderExtension, in LoadCredentialForMsalOrFailAsync:

    • process the case where credential.SourceType == CredentialSource.CustomSignedAssertion (if not skiped, return the credentials). and add
      logging
  4. Unit tests

  5. Integration tests.

    Create an integration test that demonstrate the extension (MyCustomSignedAssertionProvider)
    and the way to use it. (for instance with a daemon configuration like the following, but that's just an example)

             TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
             tokenAcquirerFactory.Services.Configure<MicrosoftIdentityApplicationOptions>(options =>
             {
                 options.Instance = "https://login.microsoftonline.com/";
                 options.TenantId = "msidlab4.onmicrosoft.com";
                 options.ClientId = "f6b698c0-140c-448f-8155-4aa9bf77ceba";
                 options.ClientCredentials = [ new CredentialDescription() { SourceType = CredentialSource.CustomSignedAssertion,
                 CustomSignedAssertionProviderName = "MyCustomExtension"}];
             });
             tokenAcquirerFactory.Services.AddCustomSignedAssertionProvider();
             var serviceProvider = tokenAcquirerFactory.Build();
    
             // Get the authorization request creator service
             IAuthorizationHeaderProvider authorizationHeaderProvider = serviceProvider.GetRequiredService<IAuthorizationHeaderProvider>();
             
             string authorizationHeader = await authorizationHeaderProvider.CreateAuthorizationHeaderForAppAsync("https://graph.microsoft.com/.default");
             Console.WriteLine(authorizationHeader.Substring(0, authorizationHeader.IndexOf(" ", StringComparison.OrdinalIgnoreCase) + 4) + "...");
             }
  6. Add documentation

    Add a wiki article to document the extensibility (probably based on the and this design proposal)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants