Search code examples
c#asp.net-coreblazor

I want to specify a non-default layout for authentication errors in Blazor


I want to specify the layout in the following error.

  • Route not found.
  • Not authorized.
  • Forbidden.

File details

App.razor

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <!-- TODO RZ9999 when Context removed -->
                    <AuthorizeView Context="authenticated">
                        <Authorized>
                            <!-- TODO ErrorLayout -->
                            <Error403/>
                        </Authorized>
                        <NotAuthorized>
                            <!-- TODO ErrorLayout -->
                            <Error401/>
                        </NotAuthorized>
                    </AuthorizeView>
                </NotAuthorized>
            </AuthorizeRouteView>
            <FocusOnNavigate RouteData="@routeData" Selector="h1"/>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(ErrorLayout)">
                <Error404/>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Error401.razor, Error403.razor, Error404.razor

<PageTitle>Error401</PageTitle>

<h3>Error401</h3>
<PageTitle>Error403</PageTitle>

<h3>Error403</h3>
<PageTitle>Error404</PageTitle>

<h3>Error404</h3>

What I have tried

According to the description in File details, ErrorLayout is used for Error404, but MainLayout is applied for Error401 and Error403.
I tried the following description, but it did not work.

Add @layout

Error401.razor as follows but MainLayoute was applied. Is @layout only valid for those with @page?

@layout ErrorLayout

<PageTitle>Error401</PageTitle>

<h3>Error401</h3>

Add LayoutView

If Error401 and Error403 are children of LayoutView, they will be nested in the MainLayout and ErrorLayout layouts.

<AuthorizeView Context="authenticated">
    <Authorized>
        <LayoutView Layout="@typeof(ErrorLayout)">
            <Error403/>
        </LayoutView>
    </Authorized>
    <NotAuthorized>
        <LayoutView Layout="@typeof(ErrorLayout)">
            <Error401/>
        </LayoutView>
    </NotAuthorized>
</AuthorizeView>

Questions

How do I write a Router to specify the layout in case of authorization and authentication errors?


Solution

  • I solved it by creating a custom version of AuthorizeRouteView.
    Create a CustomAuthorizeRouteView with the following changes based on the Blazor source AuthorizeRouteView.

    public sealed class CustomAuthorizeRouteView : RouteView
    {
    ...
        [Parameter]
        public Type NotAuthorizedLayout { get; set; }
    ...
        private void RenderContentInNotAuthorizedLayout(RenderTreeBuilder builder, RenderFragment content)
        {
            builder.OpenComponent<LayoutView>(0);
            builder.AddAttribute(1, nameof(LayoutView.Layout), NotAuthorizedLayout);
            builder.AddAttribute(2, nameof(LayoutView.ChildContent), content);
            builder.CloseComponent();
        }
    
        private void RenderNotAuthorizedInDefaultLayout(RenderTreeBuilder builder, AuthenticationState authenticationState)
        {
            var content = NotAuthorized ?? _defaultNotAuthorizedContent;
            RenderContentInNotAuthorizedLayout(builder, content(authenticationState));
        }
    ...
    }
    

    App.razor

    <CascadingAuthenticationState>
        <Router AppAssembly="@typeof(App).Assembly">
            <Found Context="routeData">
                <CustomAuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" NotAuthorizedLayout="@typeof(ErrorLayout)">
                    <NotAuthorized>
                        @if (context.User.Identity?.IsAuthenticated != true)
                        {
                            <Error401/>
                        }
                        else
                        {
                            <Error403/>
                        }
                    </NotAuthorized>
                </CustomAuthorizeRouteView>
                <FocusOnNavigate RouteData="@routeData" Selector="h1"/>
            </Found>
            <NotFound>
                <LayoutView Layout="@typeof(ErrorLayout)">
                    <Error404/>
                </LayoutView>
            </NotFound>
        </Router>
    </CascadingAuthenticationState>
    

    With this content, I was able to do what I wanted to do without navigation with the original URL.

    Full source