routesjwtblazorblazor-server-side

Blazor Web App 9 (Server / Gloabl) JWT Authentication


I am trying to implement JWT auth on a Blazor App 9 (using Server interactivity) but I can't seem to get app to route to the Login page when the user first starts the app. The Index has an [Authorize] attribute and when i run the app i get a "Page isn't working right now, error 401" instead of redirecting me to the Login page. Going to an unsecured page before trying to navigate to index works. Here is my Routes code

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

I have also added the following code to try to force it but the OnIntitializedAsync function isn't even being called.

protected override async Task OnInitializedAsync()
{
    var authState = await AuthStateProvider.GetAuthenticationStateAsync();

    if (!authState.User.Identity.IsAuthenticated)
    {
        NavigationManager.NavigateTo("login");
    }
}

The component has a simple navigation manager on the init function that routes to /login.

It seems that I have to re-learn blazor after every update.

Edit 1 I am using the builder.Services.AddCascadingAuthenticationState(); in the Program file instead of the surrounding the Router


Solution

  • After taking a look at the Identity Individual Users template for Blazor Web App 9 (with Server/Global render mode) I've managed to make JWT Auth Work!

    Step 1 - Program.cs All the code required for authentication / Authorization is as follows:

    builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(options =>
    {
    options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "your-issuer",
            ValidAudience = "your-audience",
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your-secr479458959et-ke41882928418191y1"))
        };
    
        options.Events = new JwtBearerEvents
        {
            OnChallenge = context =>
            {
                context.HandleResponse();
                context.Response.Redirect("/login");
                return Task.CompletedTask;
            }
        };
    });
    builder.Services.AddAuthorizationCore();
    

    Also add:

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

    Step 2 - CustomAuthenticationStateProvider I've used BlazoredLocalStorage to store my Token, but anything else can be used

    public class CustomAuthenticationStateProvider(ILocalStorageService localStorageService) : AuthenticationStateProvider
    {
    private readonly ILocalStorageService _localStorage = localStorageService;
    private const string TokenKey = "authToken";
    
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        try
        {
            var token = await _localStorage.GetItemAsync<string>(TokenKey);
    
            if (string.IsNullOrWhiteSpace(token))
            {
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
            }
    
            var claims = ParseClaimsFromJwt(token);
            var identity = new ClaimsIdentity(claims, "jwt");
            var user = new ClaimsPrincipal(identity);
    
            return new AuthenticationState(user);
        }
        catch (JSException ex)
        {
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
        }
        catch (Exception)
        {
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
        }
    }
    
    public void NotifyUserAuthentication(string token)
    {
        var claims = ParseClaimsFromJwt(token);
        var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"));
        var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
    
        NotifyAuthenticationStateChanged(authState);
    }
    
    public void NotifyUserLogout()
    {
        var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
        var authState = Task.FromResult(new AuthenticationState(anonymousUser));
    
        NotifyAuthenticationStateChanged(authState);
    }
    
    private static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
    {
        var handler = new JwtSecurityTokenHandler();
        var token = handler.ReadJwtToken(jwt);
    
        return token.Claims;
    }
    }
    

    Step 3 - Modify the App.razor file No clue what this actually does but i guess it is required

    <!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="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
    <link rel="stylesheet" href="@Assets["app.css"]" />
    <link rel="stylesheet" href="@Assets["BlazorApp1-Jwt.styles.css"]" />
    <ImportMap />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet @rendermode="PageRenderMode" />
    </head>
    
    <body>
    <Routes @rendermode="PageRenderMode" />
    <script src="_framework/blazor.web.js"></script>
    </body>
    
    </html>
    
    @code {
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;
    
    private IComponentRenderMode? PageRenderMode =>
        HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
    }
    

    Step 4 - Configure the Routes.razor file

    @using Microsoft.AspNetCore.Components.Authorization
    <Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
            <NotAuthorized>
                <RedirectToLogin />
            </NotAuthorized>
        </AuthorizeRouteView>
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
    </Router>
    

    This code is missing the NotFound attribute becuase it is no longer being used by Blazor Web App 8 and 9

    Step 5 - RedirectToLogin create a component that redirects to the Login page when the user is trying to access a secured page

    @inject NavigationManager NavigationManager
    
    @code {
        protected override void OnInitialized()
        {
            NavigationManager.NavigateTo("login", forceLoad: true);
        }
    }
    

    Step 6 - Login the User Aquire your Token, save it in LocalStorage (or handle it however you want) and Notify the AuthStateProvider

    ((CustomAuthenticationStateProvider)AuthenticationStateProvider).NotifyUserAuthentication(token);
    

    Step 7 - Secure Pages

    Use the Authorize attribute on any page you want secured

    Note: I am no expert. This is how i got it to work.. Please write / redirect to a better solution if what i wrote here is terrible code