Search code examples
azureazure-ad-b2cmaui

.NET MAUI, Azure B2C, different user flows with Multiple Named IPublicClientApplication Instances


I'm working on a .NET MAUI application that uses Azure AD B2C for authentication.

I have multiple Azure AD B2C policies in my application (SignUp that works fine and want to add password reset policy), and I need to use different instances of IPublicClientApplication for different policies (since they are using different user flows).

I've tried several approaches to configure the dependency injection, but I'm encountering issues where both services end up using the same IPublicClientApplication instance.

Here is the service for sign in that works fine:

public class AuthServiceB2CSignInSignUp : IAuthService
{
    private readonly IPublicClientApplication _signInApp;

    public AuthServiceB2CSignInSignUp(IPublicClientApplication signInApp)
    {
        _signInApp = signInApp;
    }
    public Task<AuthenticationResult?> SignInInteractively(CancellationToken cancellationToken)
    {
        return _signInApp
               .AcquireTokenInteractive(Constants.Scopes)
               .ExecuteAsync(cancellationToken);
    }

    public async Task<AuthenticationResult?> AcquireTokenSilent(CancellationToken cancellationToken)
    {
        try
        {
            var accounts = await _signInApp.GetAccountsAsync(Constants.SignInPolicy);
            var firstAccount = accounts.FirstOrDefault();
            if (firstAccount is null)
            {
                return null;
            }

            return await _signInApp.AcquireTokenSilent(Constants.Scopes, firstAccount)
                                             .ExecuteAsync(cancellationToken);
        }
        catch (MsalUiRequiredException)
        {
            return null;
        }
    }

    public async Task LogoutAsync(CancellationToken cancellationToken)
    {
        var accounts = await _signInApp.GetAccountsAsync();
        foreach (var account in accounts)
        {
            await _signInApp.RemoveAsync(account);
        }
    }
}

And here is the password reset that also works fine if I run it on its own:

public class AuthServiceB2CResetPassword : IAuthServiceB2CResetPassword
{
    private readonly IPublicClientApplication _resetApp;
    public AuthServiceB2CResetPassword(IPublicClientApplication resetApp)
    {
         _resetApp = resetApp;
    }
    public Task<AuthenticationResult?> SignInInteractively(CancellationToken cancellationToken)
    {
        return _resetApp
               .AcquireTokenInteractive(Constants.Scopes)
               .ExecuteAsync(cancellationToken);
    }

    public async Task<AuthenticationResult?> AcquireTokenSilent(CancellationToken cancellationToken)
    {
        try
        {
            var accounts = await _resetApp.GetAccountsAsync(Constants.SignInPolicy);
            var firstAccount = accounts.FirstOrDefault();
            if (firstAccount is null)
            {
                return null;
            }

            return await _resetApp.AcquireTokenSilent(Constants.Scopes, firstAccount)
                                             .ExecuteAsync(cancellationToken);
        }
        catch (MsalUiRequiredException)
        {
            return null;
        }
    }
}

This is the way I am registering them:

mauiAppBuilder.Services.AddScoped<IAuthService, AuthServiceB2CSignInSignUp>();
mauiAppBuilder.Services.AddScoped<IPublicClientApplication>(sp =>
{
    var app = PublicClientApplicationBuilder
        .Create(Constants.ClientId)
        .WithIosKeychainSecurityGroup(Constants.IosKeychainSecurityGroups)
        .WithRedirectUri($"msal{Constants.ClientId}://auth")
         .WithB2CAuthority(Constants.AuthoritySignIn)
        .Build();
    return app;
});


mauiAppBuilder.Services.AddScoped<IAuthServiceB2CResetPassword, AuthServiceB2CResetPassword>();
mauiAppBuilder.Services.AddScoped<IPublicClientApplication>(sp =>
{
    var passwordResetApp = PublicClientApplicationBuilder
        .Create(Constants.ClientId)
        .WithIosKeychainSecurityGroup(Constants.IosKeychainSecurityGroups)
        .WithRedirectUri($"msal{Constants.ClientId}://auth")
        .WithB2CAuthority(Constants.AuthorityPasswordReset)
        .Build();

    return passwordResetApp;
});

In my login view model I am injecting them in the constructor:

private readonly IAuthService _signupService;
private readonly IAuthServiceB2CResetPassword _passwordResetService;

public LoginViewModel(IAuthService signupService, IAuthServiceB2CResetPassword passwordResetService)
{
    _signupService = signupService;     
    _passwordResetService = passwordResetService;
}

//do something here

However no matter what the last registration instance of IPublicClientApplication always overrides the other one, since PublicClientApplication comes from Microsoft.Identity.Client I don't really understand the approach I should take in order to have two different objects of IPublicClientApplication, one for password reset and one for sign in sign up.

I tried to change the order but that didn't help either, despite configuring named instances of IPublicClientApplication, both services (_signupService and _passwordResetService) end up using the same IPublicClientApplication instance.

I've tried naming the registration, but it doesn't seem to work as expected. Is there a limitation in .NET MAUI's dependency injection system that prevents this kind of setup, or am I missing something in my configuration?

What is the correct way to configure dependency injection for multiple named IPublicClientApplication instances for different services in a .NET MAUI application? I have also tried to find some official Azure B2C MAUI examples but the one in Azure-Samples doesn't even implement the password reset, only signin signup. I would like to implement both at the same time.


Solution

  • This isn't MAUI-specific, it's just how the Microsoft-provided .NET dependency injection system works.

    With the Microsoft system when you register additional implementations for the same interface (with some caveats) it does add all implementations to the service collection but it will only return those implementation if you inject them as IEnumerable<T>. If you just inject a single interface then it will always be the last implementation registered.

    For example:

    // during service registration
    builder.Services.AddTransient<IMyDependency, MyDependencyA>();
    builder.Services.AddTransient<IMyDependency, MyDependencyB>();
    
    // when injecting multiple implementations into a service
    public class MyServiceWithMultipleDependencies
    {
        public MyServiceWithMultipleDependencies(IEnumerable<IMyDependency> dependencies)
        {
            /*
             * dependencies will contain two items: MyDependencyA and MyDependencyB
             * the implementations will be enumerated in the order they were registered
             */
        }
    }
    
    // when injecting a single implementations into a service
    public class MyServiceWithOneDependency
    {
        public MyServiceWithOneDependency(IMyDependency dependency)
        {
            // dependency will be MyDependencyB, the last implementation registered for IMyDependency
        }
    }
    
    // still counts as injecting a single implementations into a service
    public class MyServiceWithOneDependency
    {
        public MyServiceWithOneDependency(IMyDependency dependencyA, IMyDependency dependency2)
        {
            /* dependencyA will be MyDependencyB, the last implementation registered for IMyDependency
             * dependency2 will be MyDependencyB, the last implementation registered for IMyDependency
             *
             * The names of the parameters have no effect on the dependencies 
             * injected, they're just labels that make sense in the context 
             * of the service the dependency is injected into.
             */
        }
    }
    

    If you need to inject a specific implementation in the way you describe then you could use the Microsoft DI system as in this Stack Overflow answer, or you could integrate more advanced DI container such as Autofac which supports named services and the factory pattern.