I've noticed that many developers subclass the AuthenticationStateProvider
both in Blazor Server App and Blazor WebAssembly App wrongly, and more imprtantly for the wrong reasons.
How to do it correctly and when?
First off, you do not subclass the AuthenticationStateProvider
for the sole purpose of adding claims to the ClaimPrincipal
object. Generally speaking, claims are added after a user has been authenticated, and if you need to inspect those claims and tranform them, it should be done somewhere else, not in the AuthenticationStateProvider
object. Incidentally, in ASP.Net Core there are two ways how you can do that, but this merits a question of its own.
I guess that this code sample led many to believe that this is the place to add claims to the ClaimsPrincipal
object.
In the current context, implementing JWT Authentication, claims should be added to the JWT when it is created on the server, and extracted on the client when required, as for instance, you need the name of the current user. I've noticed that developers save the name of the user in the local storage, and retrieved it when needed. This is wrong: you should extract the name of the user from the JWT.
The following code sample describes how to create a custom AuthenticationStateProvider
object whose objective is to retrieve from the local storage a JWT string that has newly added,
parse its content, and create a ClaimsPrincipal
object that is served to interested parties (subscribers to the AuthenticationStateProvider.AuthenticationStateChanged
event), such as the CascadingAuthenticationState
object.
The following code sample demonstrates how you can implement a custom
AuthenticationStateProvider
properly, and for good reason.
public class TokenServerAuthenticationStateProvider :
AuthenticationStateProvider
{
private readonly IJSRuntime _jsRuntime;
public TokenServerAuthenticationStateProvider(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task<string> GetTokenAsync()
=> await _jsRuntime.InvokeAsync<string>("localStorage.getItem", "authToken");
public async Task SetTokenAsync(string token)
{
if (token == null)
{
await _jsRuntime.InvokeAsync<object>("localStorage.removeItem", "authToken");
}
else
{
await _jsRuntime.InvokeAsync<object>("localStorage.setItem", "authToken", token);
}
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var token = await GetTokenAsync();
var identity = string.IsNullOrEmpty(token)
? new ClaimsIdentity()
: new ClaimsIdentity(ServiceExtensions.ParseClaimsFromJwt(token), "jwt");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
}
And here's a code sample residing in the submit button of a Login page that calls a Web API endpoint where the user credentials are validated, after which a JWT is created and passed back to the calling code:
async Task SubmitCredentials()
{
bool lastLoginFailed;
var httpClient = clientFactory.CreateClient();
httpClient.BaseAddress = new Uri("https://localhost:44371/");
var requestJson = JsonSerializer.Serialize(credentials, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Post, "api/user/login")
{
Content = new StringContent(requestJson, Encoding.UTF8, "application/json")
});
var stringContent = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<LoginResult>(stringContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
lastLoginFailed = result.Token == null;
if (!lastLoginFailed)
{
// Success! Store token in underlying auth state service
await TokenProvider.SetTokenAsync(result.Token);
NavigationManager.NavigateTo(ReturnUrl);
}
}
Point to note: TokenProvider
is an instance of TokenServerAuthenticationStateProvider
.
Its name reflects its functionality: handling the received JWT, and providing the Access Token when requested.
This line of code: TokenProvider.SetTokenAsync(result.Token);
passes the JWT to TokenServerAuthenticationStateProvider.SetTokenAsync
in which the token is sored in the local storage, and then raises AuthenticationStateProvider.AuthenticationStateChanged
event by calling NotifyAuthenticationStateChanged
, passing an AuthenticationState
object built from the data contained in the stored JWT.
Note that the GetAuthenticationStateAsync
method creates a new ClaimsIdentity
object from the parsed JWT. All the claims added to the newly created ClaimsIdentity object are retrieved from the JWT. I cannot think of a use case where you have to create a new claim object and add it to the ClaimsPrincipal object.
The following code is executed when an authenticated user is attempting to access the FecthData
page
@code
{
private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
var token = await TokenProvider.GetTokenAsync();
var httpClient = clientFactory.CreateClient();
httpClient.BaseAddress = new Uri("https://localhost:44371/");
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"api/WeatherForecast?startDate={DateTime.Now}"));
var stringContent = await response.Content.ReadAsStringAsync();
forecasts = JsonSerializer.Deserialize<WeatherForecast[]>(stringContent, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
}
}
Note that the first line of code: var token = await TokenProvider.GetTokenAsync();
retrieves the JWT stored in the local storage, and adds it to the Authorization
header of the request.
Note: ServiceExtensions.ParseClaimsFromJwt
is a method that gets the JWT extracted from the local storage, and parse it into a collection of claims.
Your Startup class should be like this:
public void ConfigureServices(IServiceCollection services)
{
// Code omitted...
services.AddScoped<TokenServerAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<TokenServerAuthenticationStateProvider>());
}