Blazor redirect to login if user is not authenticated

Viewed 36428

I am trying to develop an app using Blazor WebAssembly and I am wondering about how I can protect my whole application if the user is not authenticated. The behavior I would implement is:

  • If an anonymous user asks for any page, then he will be redirected to the login page

Better

  • a user must be authenticated for using this app

At the moment I've implemented this behavior applying the [Authorize] attribute to every page, but I would like to centralize it.

I've achieved this goal on Blazor Server Side applying the [Authorize] attribute inside the _host.razor component.

Is there a solution even for Blazor Client Side?

9 Answers

There may be slicker ways of doing this, but this is what worked for me:

Assuming you've configured Authentication correctly according to these instructions

In your MainLayout.razor (which is used by default for all components) add a code block as follows:

@code{ 

    [CascadingParameter] protected Task<AuthenticationState> AuthStat { get; set; }

    protected async override Task OnInitializedAsync()
    {
        base.OnInitialized();
        var user = (await AuthStat).User;
        if(!user.Identity.IsAuthenticated)
        {
            NavigationManager.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}");
        }
    }
}

If the user is not authenticated, we redirect to the built-in The RemoteAuthenticatorView component at the "authentication/" enpoint with the "login" action. This should kick-off authentication

Basically to apply authorization to all pages in BlazorApp.Client, you have to add:

@attribute [Microsoft.AspNetCore.Authorization.Authorize]

...to your _Imports.razor file.

Furthermore, you can add:

@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]

...on pages that don't require authorization.

Also if you wanted to redirect a user to any page, here is something I came up with:

<NotAuthorized>
    @if (true) { navMan.NavigateTo("login"); }
</NotAuthorized>

...where navMan is an injected instance of NavigationManager. Here i'm redirecting the user to my Login.razor if they try to access an authorized user only page .

I tried the solution from @Brett and it worked but on redirect back to the page the user came from it ended up saying Authorizing..., Checking login state... and then finally Completing login... and got stuck there. The user then had to click on a link or manually type the previous URL to get back.

enter image description here

enter image description here

enter image description here

Microsoft now has documentation for "Require authorization for the entire app".

https://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/?view=aspnetcore-5.0#require-authorization-for-the-entire-app

According to the documentation you can either:

  • Use the Authorize attribute directive in the _Imports.razor file:
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]
  • Add the Authorize attribute to each Razor component in the Pages folder.

I added the code to _Imports.razor but then I only received a white screen for content:

enter image description here

I then noticed that https://localhost:44123/authentication/login also gave me a white screen that Shared\RedirectToLogin.razor normally points to. I then added @attribute [AllowAnonymous] to Pages\Authentication.razor and then everything worked as expected and I did not get stuck.

enter image description here

With this solution I could also see the default You are logged out. message.

enter image description here

You can create your own redirecting component LoginRedirect.razor.

@attribute [AllowAnonymous]
@inject NavigationManager _navigationManager

@code {
    protected override void OnInitialized()
    {
        _navigationManager.NavigateTo("/login");
    }
}

and then just use it in the App.razor like this:

<NotAuthorized>
    <LoginRedirect />
</NotAuthorized>

and dont forget to add using to that component based on your project structure on the top of the App.razor like:

@using SolutionName.ProjectName.Pages.MyFolder;

In my trip to get familiar with Blazor, I am following a tutorial and the author has a nice and clean way to solve this problem as well. As everything seems to be a component in Blazor, your login page probably is a component as well. It is at least in the tutorial. So all he does is this:

<NotAuthorized>
    <Login />
</NotAuthorized>

And you need to add the correct using to your login component of course.

The downside to this solution is that the url doesn't match the page you see when you aren't logged in.

Just add the following lines to the client ..pages/Index.razor file.

@using Microsoft.AspNetCore.Authorization
@attribute [Authorize]

This will require the Index page to be authorized on start and force the login redirect as per RedirectToLogin component used in App.Razor.

You could leverage the <NotAuthorizedContent> template of the <Router> component as described here:

<CascadingAuthenticationState>
    <Router AppAssembly="typeof(Startup).Assembly">
        <NotFoundContent>
            <p>Sorry, there's nothing at this address.</p>
        </NotFoundContent>
        <NotAuthorizedContent>
            <h1>Sorry</h1>
            <p>You're not authorized to reach this page. You may need to log in as a different user.</p>
        </NotAuthorizedContent>
        <AuthorizingContent>
            <p>Please wait...</p>
        </AuthorizingContent>
    </Router>
</CascadingAuthenticationState>

Replace the contents of <NotAuthorizedContent> with a custom component called something like RedirectToLogin whose OnInitializedAsync checks if the user is logged in, and if not, does the redirect.

Using the asp.net core identity login Page directly

The folloing solution may not hit all whishes. Here you need to make your own razor component for your login. Instead of using the identity Page from asp.net core.

<NotAuthorized>
    <Login />
</NotAuthorized>

Better is to use the one from asp.net core. The problem here is, that you get an exception if you're not checking the NavigationManager. Here is the solution without exception.

<NotAuthorized>
    @{
        try
        {
            var hasJSRuntime = navManager != null && (navManager.GetType()?.GetProperty("HasAttachedJSRuntime") != null ? 
                (bool)navManager.GetType().GetProperty("HasAttachedJSRuntime").GetValue(navManager) : true);
        
            if (hasJSRuntime)
            {
                navManager.NavigateTo("/Identity/Account/Login", true);
            }

        }
        catch
        {
                <NavLink href="Identity/Account/Login">
                    Redirect to Login
                </NavLink>
        }
    }
</NotAuthorized>

If your Blazor WASM is hosted inside .net .Server project (you have 3 projects in the solution: .Client, .Server and .Shared), add .RequireAuthorization to:

.Server/Program.cs (not the one in the .Client project)

app.MapFallbackToFile("index.html").RequireAuthorization(); 
// redirect to login page BEFORE loading Blazor WASM page if auth is missing,
// use when ALL Blazor pages require autentication
// and you don't handle login screens etc. in Blazor but in the backend

Or you can wrap your in app.razor in and add custom LoginRedirect component suggested by @ssamko but make sure you use forceful redirection if the authentication URL is handled by .Server app.

app.razor:

@using Microsoft.AspNetCore.Components.Authorization
@using YourBlazor.Client.Components

<CascadingAuthenticationState>
<AuthorizeView>
    <Authorized>
        Current user: @context.User.Identity.Name
            <Router AppAssembly="@typeof(App).Assembly">
                <Found Context="routeData">
                    <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
                    <FocusOnNavigate RouteData="@routeData" Selector="h1" />
                </Found>
                <NotFound>
                    <PageTitle>Not found</PageTitle>
                    <LayoutView Layout="@typeof(MainLayout)">
                        <p role="alert">Sorry, there's nothing at this address.</p>
                    </LayoutView>
                </NotFound>
            </Router>
    </Authorized>
    <NotAuthorized>
        <LoginRedirect />
    </NotAuthorized>
</AuthorizeView>

Components/LoginRedirect.razor (note the forceLoad:true);

@using System.Web
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
@inject NavigationManager _navigationManager

@code {
    protected override void OnInitialized()
    {
        _navigationManager.NavigateTo("/authentication/login?returnUrl=" + HttpUtility.UrlEncode(_navigationManager.ToBaseRelativePath(_navigationManager.Uri)), forceLoad:true);
        // /authentication controller is handled by hosted .Server project 
    }
}
Related