Search code examples
authenticationazure-active-directoryauthorizationmaui-blazorazure-identity

MAUI AAD Authentication ClaimsPrincipal with IsAuthenticated always false


I have a MAUI app running on localhost and when you start the app, it triggers microsoft login, after the user sucessfull login I catch the ClaimsPrincipal and IsAuthenticated is always false. According to my Main.razor component if the user is not Authenticated it calls the RedirectToLogin Component again and I don't know how to Authorize my logged in user in the app so it might have somethin to do with the IsAuthenticated being false.

I have seen some solutions and they say you have to build ClaimsPrincipal passin the authentication type as parameter to the ClaimsIdentity like this:

new ClaimsPrincipal(new Identity("something"))

The problem is that I get already the ClaimsPrincipal from AAD só I dont know what I should do because I have no way to see the AAD because it is taken care by other team.

This is my Main.razor component:

<Router AppAssembly="@typeof(Main).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
            <Authorizing>
                Authorizing...
            </Authorizing>
            <NotAuthorized>
                <RedirectToLogin />
            </NotAuthorized>
        </AuthorizeRouteView>
    </Found>
    <NotFound>
        <CascadingAuthenticationState>
            <LayoutView Layout="@typeof(MainLayout)">
                <p role="alert">Sorry, there's nothing at this address.</p>
            </LayoutView>
        </CascadingAuthenticationState>
    </NotFound>
</Router>

This is RedirectToLogin component:

@using Microsoft.AspNetCore.Components.Authorization
@using OfficeManagerApp.Areas.Services.Implementations
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject NavigationManager NavigationManager


<div class="loader loader-bouncing"><span>Redirecting...</span></div>

@code {
    
    protected override async Task OnInitializedAsync()
    {
        await ((ExternalAuthStateProvider)AuthenticationStateProvider)
            .LogInAsync();            
    
        NavigationManager.NavigateTo("/", forceLoad: true);
    }
}

and here is where I have the break point to see the CLaimsPrincipal inside the ExternalAuthStateProvider class:

ExternalAuthStateProvider

Here are the tutorials I followed for this.

PlatformService tutorial

ExternalAuthStateProvider tutorial

I also have a solution using just the PlatformService tutorial with the same problem.


Solution

  • After a while I found the solution!

    Basically you receive a ClaimsPrincipal from AAD but you have to create your own ClaimsPrincipal inside the app using the claims from the AAD ClaimsPrincipal.

    In ExternalAuthStateProvider.cs, LoginWithExternalProviderAsync() method I did the following:

            private async Task<ClaimsPrincipal> LoginWithExternalProviderAsync()
            {
                var authenticationResult = await _platformService.GetAuthenticationResult();            
    
                var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(authenticationResult.ClaimsPrincipal.Claims, "Basic"));            
    
                return await Task.FromResult(authenticatedUser);            
            }
    

    You just need to do this and then it works!!

    Extra -----------

    To improve the flow login logout, I created a LoginPage.razor:

    @page "/login"
    @using Microsoft.AspNetCore.Authorization
    @using Microsoft.AspNetCore.Components.Authorization
    @using OfficeManagerApp.Areas.Services.Implementations
    @attribute [AllowAnonymous]
    
    @inject AuthenticationStateProvider AuthenticationStateProvider
    @inject NavigationManager NavigationManager
    
    <button @onclick="Login">Log in</button>
    
    @code
    {
        public async Task Login()
        {
            await ((ExternalAuthStateProvider)AuthenticationStateProvider)
                .LogInAsync();
    
            NavigationManager.NavigateTo("/");
        }
    }
    

    Changed the RedirectToLogin,razor:

    @inject NavigationManager NavigationManager
    
    
    <div class="loader loader-bouncing"><span>Redirecting...</span></div>
    
    @code {
    
        protected override void OnInitialized()
        {        
            NavigationManager.NavigateTo("/login");
        }
    }
    

    And added a logout method:

        private void Logout(){
            ((ExternalAuthStateProvider)AuthenticationStateProvider)
                .Logout();  
        }
    

    Also did some changes to my Main.razor:

    <CascadingAuthenticationState>
        <Router AppAssembly="@typeof(Main).Assembly">
            <Found Context="routeData">
                <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                    <Authorizing>
                        Authorizing...
                    </Authorizing>
                    <NotAuthorized>
                        @if (!context.User.Identity.IsAuthenticated)
                        {
                            <RedirectToLogin />
                        }
                        else
                        {
                            <p>You are not authorized to access this resource.</p>
                        }                   
                    </NotAuthorized>
                </AuthorizeRouteView>
            </Found>
            <NotFound>      
                <LayoutView Layout="@typeof(MainLayout)">
                    <p role="alert">Sorry, there's nothing at this address.</p>
                </LayoutView>       
            </NotFound>
        </Router>
    </CascadingAuthenticationState>
    

    Note don't forget to add to your _Imports.razor:

    @using Microsoft.AspNetCore.Components.Authorization