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.
2
Answers
Based on @Dave D advice I refactored using the factory pattern(also the link to that post was very useful) I will put it here in case someone wants to reuse.
individual classes for each user flow
password reset
registration like this
each service can be then accessed individually after injecting
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:
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.