Search code examples
blazorblazor-server-side.net-8.0

Blazor server custom authentication with JWT not working in .NET 8


I have the similar code working in previous version like .NET 7, but I get "HTTP ERROR 401" error, when I add @attribute [Authorize] to the page.

Here is the sample project in GIT hub.

https://github.com/sbakula/BlazorServerCustomAuthTest

My Program.cs code;

using BlazorServerCustomAuthTest.Auth;
using BlazorServerCustomAuthTest.Components;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
            .AddJwtBearer(options =>
            {
                options.SaveToken = true;
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidAudience = "XXXX",
                    ValidIssuer = "XXXX",
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("XXXX"))
                };
            });

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
builder.Services.AddScoped<AuthStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>(
                provider => provider.GetRequiredService<AuthStateProvider>()
                );
builder.Services.AddScoped<ILoginService, AuthStateProvider>(
                provider => provider.GetRequiredService<AuthStateProvider>()
                );

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

Routes.razor code:

<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
            <NotAuthorized>
                <text>Custom not authorized...</text>
            </NotAuthorized>
        </AuthorizeRouteView>
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
</Router>

My AuthStateProvider.cs:

using Microsoft.AspNetCore.Components.Authorization;
using System.Net.Http.Headers;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using BlazorServerCustomAuthTest.Models;

namespace BlazorServerCustomAuthTest.Auth

{
    public class AuthStateProvider : AuthenticationStateProvider, ILoginService
    {
        private ProtectedSessionStorage ProtectedSessionStore;

        static AuthenticationState Anonymous =>
            new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));

        public AuthStateProvider(ProtectedSessionStorage ProtectedSessionStore)
        {
            this.ProtectedSessionStore = ProtectedSessionStore;
        }

        public async override Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            UserProfile? user;
            var result = await ProtectedSessionStore.GetAsync<UserProfile>("UserProfile");

            if (result.Success)
            {
                user = result.Value;
                return await Task.FromResult(BuildAuthenticationState(user));
            }
            else
            {
                return Anonymous;
            }
        }

        public async Task Login(UserProfile user)
        {
            await ProtectedSessionStore.SetAsync("UserProfile", user);
            var authState = BuildAuthenticationState(user);
            NotifyAuthenticationStateChanged(Task.FromResult(authState));
        }

        public async Task Logout()
        {
            await ProtectedSessionStore.DeleteAsync("UserProfile");
            NotifyAuthenticationStateChanged(Task.FromResult(Anonymous));
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }

        static AuthenticationState BuildAuthenticationState(UserProfile? userProfile)
        {
            if (userProfile is null)
            {
                return Anonymous;
            }
            else
            {
                var claims = new List<Claim> { };
                claims.Add(new Claim(ClaimTypes.Name, userProfile.UserName));
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt")));
            }
        }

    }
}

ILoginService.cs:

using BlazorServerCustomAuthTest.Models;

namespace BlazorServerCustomAuthTest.Auth
{
    public interface ILoginService
    {
        Task Login(UserProfile user);
        Task Logout();
    }
}

_Imports.razor:

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorServerCustomAuthTest
@using BlazorServerCustomAuthTest.Components
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization

App.razor:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="BlazorServerCustomAuthTest.styles.css" />
    <HeadOutlet @rendermode="new InteractiveServerRenderMode( prerender: false )" />
</head>

<body>
    <Routes @rendermode="new InteractiveServerRenderMode( prerender: false )" />
    <script src="_framework/blazor.web.js"></script>
</body>

</html>

Solution

  • I trust you are trying to integrate a custom authentication mechainsm so that when there's no user signed in the blazor app, it will redirect to the login page. But what you did in your Program.cs is adding JWT auth which will require a token in the request header otherwize returning 401 error.

    I modifty the codes li'ke below

    .AddJwtBearer(options =>
        {
            options.SaveToken = true;
            options.RequireHttpsMetadata = false;
            options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidAudience = "Test.com",
                ValidIssuer = "Test.com",
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("this is my custom Secret key for authentication"))
            };
        });
    

    and generate a jwt token to send a Get request to the home page, I can get 200 code and html content response.

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoidXNlcm5hbWUxIiwianRpIjoiMTg2MmIyYjMtMDhiYy00ZDExLThkNDctOWRiNTExMjEzMzYzIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzI3ODU4OTQ1LCJpc3MiOiJUZXN0LmNvbSIsImF1ZCI6IlRlc3QuY29tIn0.7oXqbUCX8iumocwP0dAlIeNruaofaRK_3QqwLL-PHT0
    

    enter image description here

    What you should have is a custom AuthenticationStateProvider. You can take a look at this tutorial to add login component and in that component, then call NotifyAuthenticationStateChanged method to update the authentication state.