Ada Davidsson
Ada Davidsson

Reputation: 67

Why does my Authorize attribute only work on the client and not in the controller

I have a Blazor WASM project with a Client, Server and Shared project.

Problem

When I make a Post request to my controller it doesn't want to enter the ActionResult if I add the [Authorize(Roles = "User")] attribute (even if I'm logged in). However if I add the [AllowAnonymous] attribute it works just fine.

[HttpPost]
[Route("PostOutput")]
[Authorize(Roles = "User")]
public async Task<ActionResult<string>> PostOutput([FromBody] string json)
...

But in the client it works fine either way (as long as I login first)

<AuthorizeView>
    <NotAuthorized>
        @{
            navigationManager.NavigateTo("/login");
        }
    </NotAuthorized>
    <Authorized>
...

Using that it can access the page if I login first just as you'd expect, heck it even works when I add this attribute to the page @attribute [Authorize(Roles = "User")]. So everything works as expected on the Client side, but not on the server for some reason.

Is there something super obvious that I've missed here?

Project Setup

The way I've setup the client is like this

Program.cs (Client)

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddHttpClient<IAuthService, AuthService>(client =>
{
    client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
}).AddHttpMessageHandler<CustomAuthorizationHandler>();

builder.Services.AddHttpClient<IDashboardService, DashboardService>(client =>
{
    client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
}).AddHttpMessageHandler<CustomAuthorizationHandler>();

builder.Services.AddBlazoredSessionStorage();
builder.Services.AddTransient<CustomAuthorizationHandler>();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
builder.Services.AddAuthorizationCore();
await builder.Build().RunAsync();

With the CustomAuthorizationHandler looking like this

public class CustomAuthorizationHandler : DelegatingHandler
{
    private ISessionStorageService _sessionStorageService;

    public CustomAuthorizationHandler(ISessionStorageService sessionStorageService)
    {
        _sessionStorageService = sessionStorageService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {

        var jwtToken = await _sessionStorageService.GetItemAsync<string>("UserSession");
        if (jwtToken != null)
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
        }

        return await base.SendAsync(request, cancellationToken);
    }

}

And the CustomAuthenticationStateProvider

public class CustomAuthenticationStateProvider : AuthenticationStateProvider
    {
        private readonly ISessionStorageService _sessionStorageService;
        private ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());

        public CustomAuthenticationStateProvider(ISessionStorageService sessionStorageService)
        {
            _sessionStorageService = sessionStorageService;
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            try
            {
                var userSession = await _sessionStorageService.ReadEncryptedItemAsync<UserSessionModel>("UserSession");
                if (userSession == null)
                {
                    return await Task.FromResult(new AuthenticationState(_anonymous));
                }

                var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
                {
                    new Claim(ClaimTypes.Name, userSession.Username),
                    new Claim(ClaimTypes.Role, userSession.Role)
                }, "JwtAuth"));

                return await Task.FromResult(new AuthenticationState(claimsPrincipal));
            }
            catch (Exception e)
            {
                return await Task.FromResult(new AuthenticationState(_anonymous));
            }
        }

        public async Task UpdateAuthenticationStateAsync(UserSessionModel userSession)
        {
            ClaimsPrincipal claimsPrincipal;
            if (userSession != null)
            {
                claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
                {
                    new Claim(ClaimTypes.Name, userSession.Username),
                    new Claim(ClaimTypes.Role, userSession.Role)
                }));

                userSession.ExpireDateTime = DateTime.Now.AddSeconds(userSession.ExpiresIn);
                await _sessionStorageService.SaveItemEncryptedAsync("UserSession", userSession);
            }
            else
            {
                claimsPrincipal = _anonymous;
                await _sessionStorageService.RemoveItemAsync("UserSession");
            }

            NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
        }

        public async Task<string> GetToken()
        {
            string result = String.Empty;

            try
            {
                var userSession = await _sessionStorageService.ReadEncryptedItemAsync<UserSessionModel>("UserSession");
                if (userSession != null && DateTime.Now < userSession.ExpireDateTime)
                {
                    result = userSession.Token;
                }
            }
            catch (Exception)
            {
            }
            return result;
        }

    }

SessionStorageServiceExtension.cs

public static class SessionStorageServiceExtension
{
    public static async Task SaveItemEncryptedAsync<T>(this ISessionStorageService sessionsStorageService, string key, T item)
    {
        var itemJson = JsonSerializer.Serialize(item);
        var itemJsonBytes = Encoding.UTF8.GetBytes(itemJson);
        var base64Json = Convert.ToBase64String(itemJsonBytes);
        await sessionsStorageService.SetItemAsync(key, base64Json);
    }

    public static async Task<T> ReadEncryptedItemAsync<T>(this ISessionStorageService sessionsStorageService, string key)
    {
        var base64String = await sessionsStorageService.GetItemAsync<string>(key);
        var itemJsonBytes = Convert.FromBase64String(base64String);
        var itemJson = Encoding.UTF8.GetString(itemJsonBytes);
        var item = JsonSerializer.Deserialize<T>(itemJson);
        return item;
    }
}

And the Program.cs (Server)

var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("Default") ?? throw new NullReferenceException("No connection string in config!");
// Add services to the container.

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

builder.Services.AddAuthentication(o =>
{
    o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
    o.RequireHttpsMetadata = false;
    o.SaveToken = true;
    o.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(JwtAuthenticationManager.JWT_SECURITY_KEY)),
        ValidateIssuer = false,
        ValidateAudience = false
    };
});

builder.Services.AddSingleton<ProductService>();
builder.Services.AddTransient<UserAccountService>();
builder.Services.AddDbContextFactory<CoverAIDbContext>((DbContextOptionsBuilder options) => options.UseSqlServer(connectionString));
var app = builder.Build();

Edit

Calling this from the page made it work

var customAuthStateProvider = (CustomAuthenticationStateProvider)authStateProvider;
var token = await customAuthStateProvider.GetToken();
if (!string.IsNullOrWhiteSpace(token))
{
    Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
    using var response = await Http.PostAsJsonAsync("api/Product/PostOutput", serializedString);

    if (!response.IsSuccessStatusCode)
    _output = "Error.";

    loading = false;
     buttonText = "Generate";

     _output = await response.Content.ReadAsStringAsync();
}

Which I find strange because this didn't work, even though jwtToken is valid. I have no idea what's going on here.

public class CustomAuthorizationHandler : DelegatingHandler
{
    private ISessionStorageService _sessionStorageService;

    public CustomAuthorizationHandler(ISessionStorageService sessionStorageService)
    {
        _sessionStorageService = sessionStorageService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {

        var jwtToken = await _sessionStorageService.GetItemAsync<string>("UserSession");
        if (jwtToken != null)
        {
            request.Headers.Authorization = new AuthenticationHeaderValue("bearer", jwtToken);
        }

        return await base.SendAsync(request, cancellationToken);
    }

}

So doing this in the Client doesn't work

[Inject]
public IDashboardService DashboardService { get; set; }

async Task Generate()
{
    ...
    string serializedString = System.Text.Json.JsonSerializer.Serialize(postBody);

    if (!string.IsNullOrWhiteSpace(token))
    {
        using var response = await DashboardService.Generate(serializedString);
        if (!response.IsSuccessStatusCode)
            _output = "Error.";

         _output = await response.Content.ReadAsStringAsync();
    }
    
}

But this works

@using System.Net.Http.Headers
@inject HttpClient Http

async Task Generate()
{
    ...
    string serializedString = System.Text.Json.JsonSerializer.Serialize(postBody);


    var customAuthStateProvider = (CustomAuthenticationStateProvider)authStateProvider;
    var token = await customAuthStateProvider.GetToken();
    if (!string.IsNullOrWhiteSpace(token))
    {
        Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
        using var response = await Http.PostAsJsonAsync("api/Product/PostOutput", serializedString);
        if (!response.IsSuccessStatusCode)
            _output = "Error.";

        _output = await response.Content.ReadAsStringAsync();
    }
}

Upvotes: 3

Views: 399

Answers (1)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30310

If I'm reading this code correctly, you treat the UserSession key value as different entities in the two code paths.

Using the generic HttpClient, you get the token from the CustomAuthenticationStateProvider by calling ReadEncryptedItemAsync. This returns a UserSessionModel object from the UserSession value.

// CustomAuthenticationStateProvider 
// GetToken
var userSession = await _sessionStorageService.ReadEncryptedItemAsync<UserSessionModel>("UserSession");

You then get the token from the returned UserSessionModel object.

// CustomAuthenticationStateProvider 
// GetToken
var userSession = await _sessionStorageService.ReadEncryptedItemAsync<UserSessionModel>("UserSession");
//...
result = userSession.Token;

While in the Typed HttpClient, CustomAuthorizationHandler gets the token from the ISessionStorageService using the method GetItemAsync to get UserSession as a string. You treat this as the JwtToken.

//CustomAuthorizationHandler 

var jwtToken = await _sessionStorageService.GetItemAsync<string>("UserSession");

UserSession can't be both a UserSessionModel and a JwtToken string.

Upvotes: 0

Related Questions