usausa
usausa

Reputation: 109

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

I want to specify the layout in the following error.

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?

Upvotes: 2

Views: 2504

Answers (5)

RoJaIt
RoJaIt

Reputation: 461

just want to add up on @bviala's solution:

I like this approach, but my app has got some paths that don't need authoritzation. For example the "/register" page. All private pages will automatically be redirected to the login page. The public pages all use a different layout (looks and feel of the azure bc2 login). If you only use on if statement within the <NotAuthorized> section the public pages will be rendered with the MainLayout. If want to render them with the "Login"-layout even, when you are logged in you could use the @layout LoginLayout in every page or use an if statement in the <Authorized> section like in the following example.

This is my LayoutWithAuthorisation.razor:

@inherits LayoutComponentBase

@inject NavigationManager Navigation

<AuthorizeView>
    <Authorized>
        @if (IsPublicPage())
        {
            <LayoutView Layout="typeof(LoginLayout)">
                @Body
            </LayoutView>
        }
        else
        {
            <LayoutView Layout="typeof(MainLayout)">
                @Body
            </LayoutView>
        }
    </Authorized>
    <NotAuthorized>
        <LayoutView Layout="typeof(LoginLayout)">
            @if (IsPublicPage())
            {
                @Body
            }
            else
            {
                <RedirectToLogin />
            }
        </LayoutView>
    </NotAuthorized>
    <Authorizing>
        <LayoutView Layout="typeof(LoginLayout)">
            <div class="grid align-center">
                <RadzenProgressBarCircular ShowValue="true" Mode="ProgressBarMode.Indeterminate" Size="ProgressBarCircularSize.Large">
                    <Template></Template>
                </RadzenProgressBarCircular>
            </div>
        </LayoutView>
    </Authorizing>
</AuthorizeView>

@code {
    string[] _publicPages = ["register", "users/activate"];

    private bool IsPublicPage()
    {
        string uri = Navigation.Uri.Replace(Navigation.BaseUri, string.Empty);

        return _publicPages.Any(p => uri.StartsWith(p));
    }
}

The Routes.razor file can now be simple as this:

@rendermode InteractiveServer

<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(LayoutWithAuthorisation)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>

For best practise you can consider injecting the public pages from your appsettings.json file.

Upvotes: 0

bviala
bviala

Reputation: 184

just want to add up on @edgar_wideman solution:

I think it might work fine in most scenarios, however having two @Body in the MainLayout was the source of an obscure bug for me.

I'm using AddMsalAuthentication to auth with ME-ID, and on login completion, the user would become Authorized and the layout would change, but that would retrigger a new login flow.

The solution I used is the following:

  • Remove the DefaultLayout of AuthorizeRouteView in App.razor
    This is because apparently the default layout cannot be overriden, if you specify another layout in a page, it will be merged instead
  • Have two separated layout files, each with one @Body only.
  • Home.razor like so:

<PageTitle>Home</PageTitle>

<AuthorizeView>
    <Authorized>
        <LayoutView Layout="typeof(AppLayout)">
            <h1>Hello, world!</h1>
            <p>
                Welcome to your new app.    
            </p>
        </LayoutView>
    </Authorized>
    <NotAuthorized>
        <LayoutView Layout="typeof(AuthenticationLayout)">
            <MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Large" href="authentication/login">
                Log in with Entra ID
            </MudButton>
        </LayoutView>
    </NotAuthorized>
</AuthorizeView>

Most likely the Home page is the only page where you need a conditional layout based on auth state, so I find it acceptable.

On the rest of the pages, simply declare the layout you wish to use with @layout YourLayout

Upvotes: 1

edgar_wideman
edgar_wideman

Reputation: 382

A much easier way is to set the appropriate layouts in the MainLayout.razor file. Like this

@inherits LayoutComponentBase
<!-- Page wrapper -->
<AuthorizeView>
    <Authorized>
        <div class="flex flex-row h-screen bg-slate-50 overflow-hidden">
            <!-- Side Bar-->
            <SideBar/>

            <!-- Content area -->
            <div class="">

                <!-- site header -->
                <HeaderLayout/>

                <main>
                    <div class="px-4 sm:px-6 lg:px-8 py-8 bg-slate-50">
                        @Body
                    </div>
                </main>
            </div>
        </div>
    </Authorized>
    <NotAuthorized>
        @Body
    </NotAuthorized>
    <Authorizing>
        <p>Hold up, lemma check you out.</p>
    </Authorizing>
</AuthorizeView>

Upvotes: 1

usausa
usausa

Reputation: 109

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

Upvotes: 1

Brian Parker
Brian Parker

Reputation: 14613

Is @layout only valid for those with @page?

Yes sort of... You can also add it to a layout to to specify nesting.

FYI: 401 and 403 are both captured by <NotAuthorized>.

Use a component like:

public class RedirectToPage : ComponentBase
{
    [Inject]
    private NavigationManager Navigation { get; set; }

    [Parameter]
    public string Href { get; set; }

    protected override void OnInitialized()
        => Navigation.NavigateTo(Href);
}

You could expand this to include a return address etc.

Inside your App.razor

<NotAuthorized>
@if (context.User.Identity?.IsAuthenticated == true)
{
    <RedirectToPage Href="/error403" />
}
else
{
    <RedirectToPage Href="/error401" />
}
</NotAuthorized>

Create the appropriate error pages using @layout ... of your choosing.

Upvotes: 1

Related Questions