How to maintain Authentication State with Blazor 8 Server Interactive?

Greg Finzer 0 Reputation points
2024-07-25T14:02:27.5666667+00:00

I have a setup a full example project here with a kludge where I have to manually call it to grab the state on every page. The problem is if the user refreshes the page then it will redirect to login because the Authorize will hit before the OnAfterRenderAsync

https://github.com/GregFinzer/Blazor8Auth

I want to use what is built into Blazor 8 but I am not able to figure out the correct configuration. Here is what I have in my program.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

//Authentication
builder.Services.AddAuthorization();

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = "auth_token";
        options.LoginPath = "/login";
        options.Cookie.MaxAge = TimeSpan.FromHours(24);
        options.AccessDeniedPath = "/acessDenied";
    });

builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddCascadingAuthenticationState();

builder.Services.AddBlazoredSessionStorage();
builder.Services.AddScoped<ICustomSessionService, CustomSessionService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

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

app.Run();

Here is my AuthService that I am calling on every page

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;

namespace Blazor8Auth.Services
{
    public class AuthService
    {
        const string AuthTokenName = "auth_token";
        public event Action<ClaimsPrincipal>? UserChanged;
        private ClaimsPrincipal? currentUser;
        private readonly ICustomSessionService _sessionService;
        private readonly IConfiguration _configuration;

        public AuthService(ICustomSessionService sessionService, IConfiguration configuration)
        {
            _sessionService = sessionService;
            _configuration = configuration;
        }

        public ClaimsPrincipal CurrentUser
        {
            get { return currentUser ?? new(); }
            set
            {
                currentUser = value;

                if (UserChanged is not null)
                {
                    UserChanged(currentUser);
                }
            }
        }

        public bool IsLoggedIn => CurrentUser.Identity?.IsAuthenticated ?? false;

        public async Task LogoutAsync()
        {
            CurrentUser = new();
            string authToken = await _sessionService.GetItemAsStringAsync(AuthTokenName);

            if (!string.IsNullOrEmpty(authToken))
            {
                await _sessionService.RemoveItemAsync(AuthTokenName);
            }
        }

        public async Task GetStateFromTokenAsync()
        {
            string authToken = await _sessionService.GetItemAsStringAsync(AuthTokenName);

            var identity = new ClaimsIdentity();

            if (!string.IsNullOrEmpty(authToken))
            {
                try
                {
                    var tokenHandler = new JwtSecurityTokenHandler();
                    var key = System.Text.Encoding.UTF8.GetBytes(_configuration.GetSection("AppSettings:Token").Value);

                    tokenHandler.ValidateToken(authToken, new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(key),
                        ValidateIssuer = false,
                        ValidateAudience = false,
                        ClockSkew = TimeSpan.Zero
                    }, out SecurityToken validatedToken);

                    var jwtToken = (JwtSecurityToken)validatedToken;
                    identity = new ClaimsIdentity(jwtToken.Claims, "jwt");
                }
                catch
                {
                    await _sessionService.RemoveItemAsync(AuthTokenName);
                    identity = new ClaimsIdentity();
                }
            }

            var user = new ClaimsPrincipal(identity);
            CurrentUser = user;
        }


        public async Task Login(ClaimsPrincipal user)
        {
            CurrentUser = user;

            var tokenEncryptionKey = _configuration.GetSection("AppSettings:Token").Value;
            var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8
                .GetBytes(tokenEncryptionKey));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
            var tokenHoursString = _configuration.GetSection("AppSettings:TokenHours").Value;
            int.TryParse(tokenHoursString, out int tokenHours);
            var token = new JwtSecurityToken(
                claims: user.Claims,
                expires: DateTime.Now.AddHours(tokenHours),
                signingCredentials: creds);

            var jwt = new JwtSecurityTokenHandler().WriteToken(token);
            await _sessionService.SetItemAsStringAsync(AuthTokenName, jwt);
        }
    }
}

Here is the code that has to be on every page:

@code {
    [Inject] private AuthService AuthService { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!AuthService.IsLoggedIn)
        {
            await AuthService.GetStateFromTokenAsync();
        }
    }
}
Blazor
Blazor
A free and open-source web framework that enables developers to create web apps using C# and HTML being developed by Microsoft.
1,596 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Bruce (SqlWork.com) 66,706 Reputation points
    2024-07-25T17:26:05.2566667+00:00

    your code and goal is not clear. you enable cookie authentication support, but have no cookie login support. you have code to create and store a jwt token in session instead of the authentication cookie. you use the AuthService instead of the authentication provider to detect login, even though you enabled cascading authentication state.

    create a new blazor web app with individual accounts to get a better starting template:

    % dotnet new blazor -au Individual

    the template uses blazor pages, identity framework and EF to handle login.

    note: if you planed on using oauth, this requires cookie authenication and redirecting to the oauth server. again you use the razor components, and its the same as adding oauth to a razor page app. if you need the to use the access/refresh tokens see:

    https://learn.microsoft.com/en-us/aspnet/core/blazor/security/server/additional-scenarios?view=aspnetcore-8.0#pass-tokens-to-a-server-side-blazor-app


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.