Reputation: 67
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
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