realmvpisme
realmvpisme

Reputation: 55

Enforce Global Authorization Requirement in Blazor Server

I have a Blazor server app in which I've implemented authentication/authorization using ASP.Net Core Identity. My goal is to require authorization globally. If a user browses to any route, valid or not, and they haven't signed in, they are to be redirected to the login page.

The process currently only works partially. Lets say I'm running the app and I haven't signed in yet. I'm getting three different responses depending on which action I take.

I've tried to handle this a couple of different ways. Here's where I'm at so far.

App.razor

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

_Imports.razor

@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using Blazored.Modal
@using Blazored.Modal.Services
@attribute [Authorize]

RedirectToLogin.razor

@using System.Security.Claims
@inject NavigationManager Navigation

@code {
    [CascadingParameter] private Task<AuthenticationState> AuthenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        ClaimsPrincipal authenticationState = (await AuthenticationState).User;

        if (!authenticationState.Identity.IsAuthenticated)
        {
            var returnUrl = Navigation.ToBaseRelativePath(Navigation.Uri);

            if (String.IsNullOrWhiteSpace(returnUrl))
            {
                Navigation.NavigateTo("/identity/account/login", true);
                
            }

        }
    }
}

_LoginPartial.cshtml

@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@attribute [AllowAnonymous]
@inject SignInManager<UserModel> SignInManager
@inject UserManager<UserModel> UserManager
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<ul class="navbar-nav">
    @if (SignInManager.IsSignedIn(User))
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a>
        </li>
        <li class="nav-item">
            <form class="form-inline" asp-area="Identity" asp-page="/Identity/Account/Logout" asp-route-returnUrl="/" method="post">
                <button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
            </form>
        </li>
    }
    else
    {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a>
        </li>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a>
        </li>
    }
</ul>

I'm alright with adding an @attribute[Authorize] to each razor page if that's what it takes. Most of them will probably get one anyways once I introduce roles. I definitely want the default action for an unauthenticated user to be to redirect to the login page. What am I missing?

Upvotes: 1

Views: 2495

Answers (2)

ellefry
ellefry

Reputation: 1

  1. Add [Authorize] to you backend api controller

  2. Regsiter 'OnRedirectToLogin' ,e.g.

    if (context.Request.Path.Value.StartsWith("/api")) { context.Response.Clear(); context.Response.StatusCode = 401; return Task.FromResult(0); }

  3. Intercept HttpClient

    builder.Services.AddScoped(sp => new HttpClient( sp.GetRequiredService<CustomAuthorizationMessageHandler>()) { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

  4. `public class CustomAuthorizationMessageHandler : DelegatingHandler { private readonly NavigationManager navManager; public CustomAuthorizationMessageHandler(NavigationManager navManager, HttpMessageHandler innerHandler=null) { InnerHandler = InnerHandler = innerHandler ?? new HttpClientHandler(); this.navManager = navManager; }

     protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
     {
         var response = await base.SendAsync(request, cancellationToken);
         var status = response.StatusCode;
         if (status == HttpStatusCode.Unauthorized) //throw new ApplicationException(status.ToString());
         {
             navManager.NavigateTo("login");
         }
         return response;
     }
    

    }`

Upvotes: 0

enet
enet

Reputation: 45626

Not sure why you write ASP.Net Identity 4. You probably mean Asp.Net Core Identity, right ?

I'm alright with adding an @attribute[Authorize] to each razor page if that's what it takes

But you already have @attribute[Authorize]in the _Imports.razor file, which makes adding this directive to each razor file unnecessary.

I'm not sure I understand the issue. If your _Imports.razor file contains @attribute[Authorize], then unauthorized user is automatically redirected to the login page...

The RedirectToLogin component should only contain code to perform the redirection. It should not contain code to verify if the user is authenticated or not. Code that redirect to the RedirectToLogin component should contain the code that performs this checking. In your case, from AuthorizeRouteView.NotAuthorized Here's how your code should look like:

<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
              
                    @if (!context.User.Identity.IsAuthenticated)
                    {
                       var returnUrl =
               NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
                <RedirectToLogin ReturnUrl="@returnUrl" />

                        
                    }
                    else
                    {
                        <p>You are not authorized to access this resource.</p>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>

Note that when the user is <NotAuthorized>, we check whether he's authenticated or not, if he's not authenticated, we render the RedirectToLogin component whose code is :

@inject NavigationManager NavigationManager

@code{
    
    [Parameter]
    public string ReturnUrl { get; set; }
    protected override void OnInitialized()
    {
        ReturnUrl = "~/" + ReturnUrl;
        NavigationManager.NavigateTo($"Identity/Account/Login?returnUrl= 
             {ReturnUrl}", forceLoad:true);
    }
}

However, if the user is authenticated, we display the message:

You are not authorized to access this resource. As you may realize by now, a user may be authenticated and still not authorized to access a given resource.

The following is the complete code that should be in your App.razor file

@inject NavigationManager NavigationManager

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @if (!context.User.Identity.IsAuthenticated)
                        {
                           var returnUrl =
                   NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
                    <RedirectToLogin ReturnUrl="@returnUrl" />

                            
                        }
                        else
                        {
                            <p>You are not authorized to access this resource. 
                            </p>
                        }
                    </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

Note that the CascadingAuthenticationState should be used once only, and not as you did.

Upvotes: 2

Related Questions