.netmauiazure-ad-b2cazure-ad-msalmsal

Creating authentication flow in .NET MAUI app for Azure AD B2C


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

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


Solution

  • 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.