Reputation: 55
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.
If the browser is directed to the main route, I'm automatically redirected to the login page so we're good there.
If I try to browse directly to another valid route, a blank screen is displayed and I am not redirected.
If I try to browse to an invalid route, part of the nav bar is displayed and the child component is blank. Still no redirect.
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
Reputation: 1
Add [Authorize] to you backend api controller
Regsiter 'OnRedirectToLogin' ,e.g.
if (context.Request.Path.Value.StartsWith("/api")) { context.Response.Clear(); context.Response.StatusCode = 401; return Task.FromResult(0); }
Intercept HttpClient
builder.Services.AddScoped(sp => new HttpClient( sp.GetRequiredService<CustomAuthorizationMessageHandler>()) { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
`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
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