ASP.NET Web API, Blazor, Authentication: Mixed topics


The following post is an attempt to describe one potential solution of a simple application with an ASP.NET Web API backend and a Blazor WebAssembly frontpage with some form of authentication:

ASP.NET WebAPI & Blazor WebAssembly

This post is a compilation of topics and troubleshooting notes that might be useful for implementing a simple ASP.NET WebAPI & Blazor WebAssembly application.

Troubleshooting

JWT is not well-formed

Problem

We have a Blazor front page. After successful authentication we set the JWT token with the following code:

await jsr.InvokeVoidAsync("localStorage.setItem", "jwtToken", $"{result.email};{result.jwtBearer};{DateTime.Now}").ConfigureAwait(false);

And to check whether we have already successfully authenticated we use the following code:

var tokenStr = await jsr.InvokeAsync<string>("localStorage.getItem", "jwtToken");

if (tokenStr == null)
{
	return;
}

var jsonToken = new JwtSecurityTokenHandler().ReadToken(tokenStr);

var token = jsonToken as JwtSecurityToken;

var claim = token.Claims.Where(c => c.Type == "email").FirstOrDefault();

During the loading of the affected Blazor login page, we get the following error which occurs during this call of the ReadToken() command:

Unhandled exception rendering component: IDX12709: CanReadToken() returned false. JWT is not well formed: '[PII of type 'System.String' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
      The token needs to be in JWS or JWE Compact Serialization Format. (JWS): 'EncodedHeader.EndcodedPayload.EncodedSignature'. (JWE): 'EncodedProtectedHeader.EncodedEncryptedKey.EncodedInitializationVector.EncodedCiphertext.EncodedAuthenticationTag'.
System.ArgumentException: IDX12709: CanReadToken() returned false. JWT is not well formed: '[PII of type 'System.String' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
The token needs to be in JWS or JWE Compact Serialization Format. (JWS): 'EncodedHeader.EndcodedPayload.EncodedSignature'. (JWE): 

Resolution

This error indicates that the token we are trying to read from the local storage is not in a valid format. The JwtSecurityTokenHandler.ReadToken() method expects a string in the JWS Format, meaning that the string should contain the encoded header, payload, and signature. But that’s not the format we are storing with localStorage.setItem() method.

Here is a potential corrected version of the code:

We prepare the JWT Token like this on the server side:

[AllowAnonymous]
[HttpPut]
// This is the API method which is called from the frontend to login a user
public async Task<LoginResult> Put(Credential credential)
{
    var user = _authenticate(credential);
    if (user != null)
    {
        var token = _generateToken(user);
        return new LoginResult { message = "Login successful.", jwtBearer = _generateToken(user), email = user.Email, success = true };
    }

    return new LoginResult { message = "User/password not found.", success = false };
}

// Generates and returns a JWT token
private string _generateToken(User user)
{
    var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configiguration["Jwt:Key"]));
    var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
    var claims = new[]
    {
        new Claim(ClaimTypes.NameIdentifier,user.Login),
        new Claim(ClaimTypes.Role,user.Role)
    };

    var token = new JwtSecurityToken(_configiguration["Jwt:Issuer"],
        _configiguration["Jwt:Audience"],
        claims,
        expires: DateTime.Now.AddMinutes(15),
        signingCredentials: credentials);

    return new JwtSecurityTokenHandler().WriteToken(token);
}

And then set the received token as follows on the Blazor client side:

LoginResult result = await msg.Content.ReadFromJsonAsync<LoginResult>();

if (result.success)
{
    // Serialize the token to a JWT string in the JWS Compact Serialization Format
    //var jwtString = new JwtSecurityTokenHandler().WriteToken(result.jwtBearer);

    // Save the JWT string to the local storage
    await jsr.InvokeVoidAsync("localStorage.setItem", "jwtToken", result.jwtBearer).ConfigureAwait(false);
}