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

[Feature Request] Improved support for inheriting / customizing MicrosoftIdentity*AuthenticationHandler #1667

Closed
MattKotsenas opened this issue Mar 23, 2022 · 4 comments
Assignees
Labels
Milestone

Comments

@MattKotsenas
Copy link
Contributor

Is your feature request related to a problem?

My specific scenario is likely niche, but I think the capability would be broadly applicable. For context, in my case I have a web API that makes protected downstream web API calls using the AddMicrosoftIdentityAppAuthenticationHandler route. I chose this method over IDownstreamApi because I have a set of HttpClient delegating handlers (like retry and backoff policies) that vary per HttpClient type.

My specific scenario is that my downstream API has a mix of authenticated and anonymous endpoints, and the AzureAd config provided to each instance of my web API may be different and may have been misconfigured by the user. That means that if the user provides a bad configuration, all downstream API calls will fail, even the anonymous ones, because there's no try...catch around token acquisition in MicrosoftIdentityAppAuthenticationMessageHandler.cs (see

var authResult = await TokenAcquisition.GetAuthenticationResultForAppAsync(
options.Scopes!,
options.AuthenticationScheme,
options.Tenant,
options.TokenAcquisitionOptions)
.ConfigureAwait(false);
) and thus failing to acquire a token is an uncaught exception.

Describe the workaround you have today

Today I work around this issue by copying the entirety of MicrosoftIdentityAppAuthenticationMessageHandler.cs into my library to add the try...catch, and then additionally I need to create a new override of AddMicrosoftIdentityAppAuthenticationHandler with the implementation copied so that I can add my "safe" handler as the HTTP message handler (see

).

Describe the solution you'd like

Depending on the level of support that makes sense for a scenario like this, I propose a tiered approach:

Tier 1: Register and retrieve the MicrosoftIdentity*AuthenticationHandler classes from DI

If instead of calling new directly on the handler classes in the extension methods, if the handlers were registered as dependencies and resolved from the ServiceCollection, I could inherit from the handler to do my custom work, which would eliminate the need to also duplicate the extension method. Bonus points if the authentication handlers gain an interface.

Tier 2: Move token acquisition into a virtual method in the handlers

In this tier, if we moved the token acquisition, i.e.:

var authResult = await TokenAcquisition.GetAuthenticationResultForAppAsync(
    options.Scopes!,
    options.AuthenticationScheme,
    options.Tenant,
    options.TokenAcquisitionOptions)
    .ConfigureAwait(false);

into a virtual method, then I could override just this method and add a try...catch, which would avoid needing to re-implement the rest of the class.

Describe alternatives you've considered

I added a section above with the workaround I have today. Alternatively, I could avoid the situation all together by splitting my downstream API into two different HttpClients, one for the authenticated endpoints and another for the anonymous endpoints, however in addition to having some negative performance characteristics (e.g. double TCP connections open to the same host), it also would lead to a suboptimal developer experience for my users, as the two sets of APIs aren't logically distinct, so it wouldn't be clear why some APIs use one client and some APIs use another.

Let me know if this request makes sense or not, and if you have any questions. Thanks!

@MattKotsenas MattKotsenas added enhancement New feature or request feature request labels Mar 23, 2022
@MattKotsenas
Copy link
Contributor Author

I created a branch with an example of the type of hooks I'm thinking of here: https://github.com/MattKotsenas/microsoft-identity-web/commits/feature/di-auth-message-handlers

Each commit showcases a specific idea, the second shows adding an abstract method that centralizes the call to TokenAcquisition, so that inheritors have a seam to add new behavior, and the first shows adding a factory so that I can override the implementation of the handler, which I can't easily do today without also reimplementing the extension method itself. I don't love adding the factory interface, but I wasn't sure of another way to add the handlers to the DI container and still be able to resolve named options instances. If I'm missing something there please let me know.

With these two changes together, my service would override the behavior with something like this:

public class Startup
{
        public void ConfigureServices(IServiceCollection services)
        {
            // register other services
            services.Replace(ServiceDescriptor.Singleton<IDelegatingHandlerFactory, MyDelegatingHandlerFactory>());
        }
}

internal class MyDelegatingHandlerFactory : IDelegatingHandlerFactory
{
    public DelegatingHandler CreateAppHandler(IServiceProvider serviceProvider, string? name)
    {
        return new AuditingMicrosoftIdentityAppAuthenticationMessageHandler(
            serviceProvider.GetRequiredService<ITokenAcquisition>(),
            serviceProvider.GetRequiredService<IOptionsMonitor<MicrosoftIdentityAuthenticationMessageHandlerOptions>>(),
            serviceProvider.GetRequiredService<ILogger<AuditingMicrosoftIdentityAppAuthenticationMessageHandler>>(),
            name);
    }

    public DelegatingHandler CreateUserHandler(IServiceProvider serviceProvider, string? name)
    {
        // ...snip for brevity...
    }
}

internal class MyAppAuthenticationMessageHandler : MicrosoftIdentityAppAuthenticationMessageHandler
{
    private readonly ILogger<MyAppAuthenticationMessageHandler > _logger;

    public MyAppAuthenticationMessageHandler (
        ITokenAcquisition tokenAcquisition,
        IOptionsMonitor<MicrosoftIdentityAuthenticationMessageHandlerOptions> namedMessageHandlerOptions,
        ILogger<MyAppAuthenticationMessageHandler > logger,
        string? serviceName = null)
        : base(tokenAcquisition, namedMessageHandlerOptions, serviceName)
    {
        _logger = logger;
    }

    protected override async Task<AuthenticationResult> GetTokenAsync(MicrosoftIdentityAuthenticationMessageHandlerOptions options)
    {
        try
        {
            return await base.GetTokenAsync(options).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Can't get token");
        }

        // add other logic; empty token here as an example
        return new AuthenticationResult(string.Empty, false, string.Empty, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, string.Empty, null, string.Empty, new string[] { }, Guid.NewGuid());
    }
}

Please let me know if this example helps illustrate my situation, and if changes along these lines seem reasonable. If there's also a different way to be approaching this problem, please let me know! Thanks!

@jmprieur
Copy link
Collaborator

@MattKotsenas do we want to try that you submit a PR?

@MattKotsenas
Copy link
Contributor Author

Sure! I've opened both #1674 and #1675, since they're technically independent and can be considered separately. Let me know if there's anything else that's helpful, or if you have any questions!

@jennyf19
Copy link
Collaborator

Released in 1.24.0.

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

No branches or pull requests

3 participants