I'm using MSAL.NET in my .NET MAUI app to handle identity management and acquiring access_token
's from Azure AD B2C.
The behavior I want is that all users first hit the StartupPage.xaml
page and
HomePage.xaml
LoginPage.xaml
so that they can initiate the login process by
clicking a button. I do not want a forced-login process where the
browser pops open asking for login credentials which is what's
happening with my current code -- see below.This is the code in InitAsync()
method of the view model for the StartupPage.xaml
:
internal async Task InitAsync()
{
await PublicClientSingleton.Instance.AcquireTokenSilentAsync();
var claims = PublicClientSingleton.Instance.MSALClientHelper.AuthResult.ClaimsPrincipal.Claims;
if(claims == null)
// Login process failed and the user is not authenticated. Send user to login page
else
// Send user to home page
}
With this code, if the user has previously logged in, it works nicely and acquires new tokens and sends the user to HomePage.xaml
. However, if the user is opening the app for the first time, it abruptly opens the browser asking for login credentials.
Clearly, calling await PublicClientSingleton.Instance.AcquireTokenSilentAsync();
will automatically launch the browser if the user hasn't signed in yet.
How should I check if the user previously logged in so that if he hasn't I can send the user to LoginPage.xaml
and let the user initiate the login process manually by clicking a button?
P.S. The MSAL.NET code I'm using came from this repo by Microsoft: https://github.com/Azure-Samples/ms-identity-dotnetcore-maui
I'm not sure if this is the best way but it seems clean and it works.
Basically, the authentication process seems to end up in SignInUserAndAcquireAccessToken(string[] scopes);
method in MSALClientHelper.cs
and this method determines whether the authentication process should continue "interactively" or not i.e. prompt the user with Azure AD B2C login page in a browser
I modified this method slightly by adding a bool shouldPromptLogin
parameter and if the value is false
, I don't go ahead with interactive login and return a null
value instead.
The modified version of this method looks like this:
public async Task<string> SignInUserAndAcquireAccessToken(string[] scopes, bool shouldPromptLogin)
{
Exception<NullReferenceException>.ThrowOn(() => this.PublicClientApplication == null, PCANotInitializedExceptionMessage);
var existingUser = await FetchSignedInUserFromCache().ConfigureAwait(false);
try
{
// 1. Try to sign-in the previously signed-in account
if (existingUser != null)
{
this.AuthResult = await this.PublicClientApplication
.AcquireTokenSilent(scopes, existingUser)
.ExecuteAsync()
.ConfigureAwait(false);
}
else
{
if (shouldPromptLogin)
this.AuthResult = await SignInUserInteractivelyAsync(scopes);
else
return null;
}
}
catch (MsalUiRequiredException ex)
{
// A MsalUiRequiredException happened on AcquireTokenSilentAsync. This indicates you need to call AcquireTokenInteractive to acquire a token interactively
Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
this.AuthResult = await this.PublicClientApplication
.AcquireTokenInteractive(scopes)
.ExecuteAsync()
.ConfigureAwait(false);
}
catch (MsalException msalEx)
{
Debug.WriteLine($"Error Acquiring Token interactively:{Environment.NewLine}{msalEx}");
}
return this.AuthResult != null && !string.IsNullOrWhiteSpace(this.AuthResult.AccessToken) ? this.AuthResult.AccessToken : null;
}
Obviously, I had to add the bool shouldPromptLogin
parameter to all the other methods that lead to calling this method so that I can pass the bool
value down the chain.
So now, in my view model for StartupPage.xaml
, I simply set the value to false
and end up getting a null
result if the user never logged in before. This way I can send the user to my LoginPage.xaml
.
And in the LoginPage.xaml
, I call this method by setting the value for shouldPromptLogin
to true
.
The initialization method in the view model for StartupPage.xaml
looks like this:
internal async Task InitAsync()
{
await PublicClientSingleton.Instance.AcquireTokenSilentAsync(false);
var claims = PublicClientSingleton.Instance.MSALClientHelper.AuthResult.ClaimsPrincipal.Claims;
if(claims == null)
await Shell.Current.GotoAsync(nameof(LoginPage));
else
await Shell.Current.GotoAsync(nameof(HomePage));
}
And in the click event for login button in LoginPage.xaml
, I call this method with a true
value so that MSAL can prompt the user with the login page hosted by Azure AD B2C. That looks like this:
[RelayCommand]
async Task LoginButtonClicked()
{
await PublicClientSingleton.Instance.AcquireTokenSilentAsync(true);
PublicClientSingleton.Instance.MSALClientHelper.AuthResult.ClaimsPrincipal.Claims;
}
I'd appreciate it if someone could post an answer with a better approach if there's one. Thanks.