ASP.NET WebAPI & Blazor WebAssembly


Creating the template-based solution

This is an example project on how to create a web application with an ASP.NET WebAPI backend and a Blazor WebAssembly frontend.

We use VS 2022 for this example.

Select project template [Blazor Webassembly App]:

Select the option [ASP.NET Core hosted]. This will create the ASP.NET WebAPI backend:

VS creates a solution with three projects (here we have named the project CollaBlocks: It is supposed to become a collaborations software where people collaborate on writing blocks which can be text blocks or other types of blocks):

  • The CollaBlocks.Client project: This is the Blazor WebAssembly based frontend
  • The CollaBlocks.Server project: This is the ASP.NET WebAPI backend
  • The CollaBlocks.Shared project: This project contains the shared classes used by both front and backend.

Server-side

Add authentication

We would like to add JWT (https://jwt.io/introduction) based authentication functionality to our web application.

Add the following packages to the WebAPI backend project:

Microsoft.AspNetCore.Authentication.JwtBearer
Microsoft.IdentityModel.Tokens
System.IdentityModel.Tokens.Jwt

Next we have to set the parameters required for JWT in appsetting.json:

  "Jwt": {
    "Key": a-random-string-with-more-than-32-characters, 
    "Issuer": url-of-the-webapi, 
    "Audience": url-of-the-webapi
  }

For the random string you can get help from an online random string generator like https://www.random.org/strings/

The url of the WebAPI can be seen once you start the application or in the Properties/launchSettings.json file of the backend project. To make the configuration easier and avoid the hassle to install/configure a SSL certificate we choose the http address:

Properties/launchSettings.json:

{
...
"applicationUrl": "https://localhost:7190;http://localhost:5190",
...
}

Add the following JWT related code to programs.cs of the WebAPI:

CollaBlocks\Server\Programs.cs:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

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

// Prepare JWT Authentication 
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => {
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
    };
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error");
    // 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.UseBlazorFrameworkFiles();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");

app.Run();

We require a class to represent the user, which we add to the shared project so that it can be available for both the backend and frontend.

User.cs:

namespace CollaBlocks.Shared
{
    public class User
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Login { get; set; }
        public string Password { get; set; }
        public string Role { get; set; }
    }
}

Then we need a class that contains the data which the frontend sends to the backend when a user logs in:

Credential.cs:

namespace CollaBlocks.Shared
{
    public class Credential
    {
        public string Login { get; set; }
        public string Password { get; set; }
    }
}

Login test

To allow a user to log in we add a LoginController class to our backend application represented by CollaBlocks.Server project (one of the three projects automatically created by VS 2022). The LoginController class uses also the class LoginResult that is shared between Server and Client:

LoginResult.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CollaBlocks.Shared
{
    public class LoginResult
    {
        public string message { get; set; }
        public string email { get; set; }
        public string jwtBearer { get; set; }
        public bool success { get; set; }
    }
}

LoginController.cs:

using CollaBlocks.Shared;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace CollaBlocks.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class LoginController : ControllerBase
    {
        private readonly IConfiguration _configiguration;
        public LoginController(IConfiguration config)
        {
            _configiguration = config;
        }

        // To make things simpler we have the users hard coded instead in a DB
        private static List<User> _registeredUers = new()
        {
            new User()
            {
                FirstName = "Ishaan",
                LastName = "Vikram",
                Login = "ivi",
                Password = "123456",
                Role = "Admin"
            },
            new User()
            {
                FirstName = "Meng",
                LastName = "Chongda",
                Login = "mch",
                Password = "abcdef",
                Role = "User"
            },
            new User()
            {
                FirstName = "Mike",
                LastName = "Green",
                Login = "mgr",
                Password = "654321",
                Role = "User"
            },
        };

        [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);
        }

        // Authenticates the user: Returns the loggend in user in case the user exists and its password matches
        private User _authenticate(Credential credential)
        {
            var loggedInUser = _registeredUers.FirstOrDefault(usr =>
                usr.Login.ToLower() == credential.Login.ToLower() && usr.Password == credential.Password);
            if (loggedInUser != null)
            {
                return loggedInUser;
            }
            return null;
        }
    }
}

This class has a Put API method which can be called by an HTTP put request to http://localhost:13831/api/login

An object of type Credentials is passed to this API method.

The Put method does two things,

  • it calls the _authenticate() method to check whether the credentials match any registered user and password.
  • And if the credentials match it calls the _generateToken() method to generate a token which is sent to the front page, which allows the logged-in user to access other pages without the need to go through the login page anymore.

To test our login API we can use Postman. Because we don’t want to configure a certificate for SSL calls, we try to access the Login API using the non-SSL address http://localhost:5190 we mentioned before.

But we have also to make sure that the application does not enforce SSL. So look for the following line in the Programs.cs file of the CollaBlock.Server Backend application and uncomment it for now:

//app.UseHttpsRedirection();

The following screenshot shows how we can test our API with Postman:

To check that the API method put is really called, set a break point in the Put method of the LoginController class, and start the application (with CollaBlocks.Server and not with IIS Express profile):

Send the request from Postman:

If everything works as expected you should land at the breakpoint in the Put() method of LoginController:

...
[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 };
}
...

Then if you step further with the debugger the _authenticate() method is called and returns the user which matches the credentials and then _generateToken() is called which returns a token. If you continue the program that token will be returned to Postman:

Authorization test

Now we have a simple login API class that is capable of processing credentials and generating a token. This token can now be used to allow access to other APIs of the application.

Let’s create another API class that should be only accessible using the token generated by the login API:

BlockController.cs:

using CollaBlocks.Shared;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace CollaBlocks.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BlockController : ControllerBase
    {
        // Show the admin panel if user has Admin role
        [HttpGet]
        [Route("AdminPanel")]
        [Authorize(Roles = "Admin")]
        public IActionResult AdminPanel()
        {
            return Ok($"Show Admin panel");
        }

        // Show the user panel if user has User role
        [HttpGet]
        [Route("UserPanel")]
        [Authorize(Roles = "Admin, User")]
        public IActionResult UserPanel()
        {
            return Ok($"Show User panel");
        }
    }
}

In this class, we have two different methods AdminPanel() and UserPanel(). Using the attribute [Authorize(Roles = “…”)] we can define which user roles are allowed to access the corresponding method.

For test purposes, these methods do nothing for now and just return a message each to show that the corresponding method could be accessed.

We can test these methods using Postman with the following links:

http://localhost:5190/api/Block/AdminPanel

http://localhost:5190/api/Block/UserPanel

To test the authorization proceed as follows:

  • Prepare a Postman GET request to http://localhost:5190/api/Block/AdminPanel or http://localhost:5190/api/Block/UserPanel
  • First use the Postman request mentioned in the “Login test” section to get a token, either for a user with Amin or for a user with User role.
  • Then open the new Postman request and select the Authorization type “Bearer Token” and paste the token into the new request.
  • Set the breakpoint in VS in the corresponding method in the code.
  • Then send the request.

If everything works as expected the request is sent to the running application and we hit the breakpoint in the corresponding method. Continue the program and then we see the corresponding response (depending on which role you used to get the token in the login request)

Client-side

Login page

First version

So far we have in the Web API project (CollaBlock.Server) two Controllers, the LoginController which has an API method to handle the Login process and the BlockController which has API methods which can be accessed by a valid token:

Now we proceed by adding a Login.razor page in the Client project (CollaBlocks.Client):

Add the following content to the newly added razor file:

Login.razor:

@page "/login"
@using CollaBlocks.Shared
@inject HttpClient Http

<PageTitle>Login</PageTitle>

<h1>Login</h1>

<table>
    <tr>
        <td>Login:</td>
        <td><input Id="idLogin" @bind="_login" /></td>
    </tr>
    <tr>
        <td>Password:</td>
        <td><input Id="idPassword" @bind="_password" /></td>
    </tr>
    <tr>
        <td><button class="btn btn-primary" @onclick="_submitCredentials">Login</button></td>
    </tr>
</table>

@code {
    string _login = "";
    string _password = "";
    private async Task _submitCredentials()
    {
        var credential = new Credential()
            {
                Login = _login,
                Password = _password
            };
        await Http.PutAsJsonAsync("api/login", credential);
    }
}

The Login page would look as follows:

For the [Login] button’s onclick event we have implemented the handler _submitCredentials(). Inside this method, we send a request, with the credentials entered in the GUI form, to the login API we tested in the “Login test” section.

To add the new Login.razor page to the site’s navigation menu you have to update the file NavMenu.razor:

Add the Login page to the navigation menu:

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">CollaBlocks</a>
        <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
            <span class="navbar-toggler-icon"></span>
        </button>
    </div>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="counter">
                <span class="oi oi-plus" aria-hidden="true"></span> Counter
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="fetchdata">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="login">
                <span class="oi oi-list-rich" aria-hidden="true"></span> Login
            </NavLink>
        </div>
    </nav>
</div>

@code {
    private bool collapseNavMenu = true;

    private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

Run the application, go to the Login page and enter the credentials for a valid user (users are currently hardcoded in the _registeredUers variable of the LoginController class. If you click on the [Login] button, you should hit the breakpoint in the Put() method of the LoginController API and you should be able to see the entered credentials:

Second version

Our simple login page is so far capable of sending the credentials entered by the user to the server side and having the LoginController perform the validation of user credentials and generation of JWT token, but our login page does nothing more than that. Now we want to change our login page so that it starts to do something with the token that is returned from LoginController. We want the login page to:

  • Store that token in local storage (it’s not the safest thing to do but for our simple example it’s good enough). You can check this post for an example of how to use local storage.
  • Set the authentication state of the user based on the stored token.
  • Show credential fields (login & password) or a message about being already logged in depending on whether the user has already logged in or not.

The following expanded version of the login page can handle these two additional requirements:

@page "/login"
@using CollaBlocks.Shared
@using System
@using System.IdentityModel.Tokens.Jwt
@using System.Security.Claims
@using System.Text
@using Microsoft.IdentityModel.Tokens
@inject HttpClient Http
@inject IJSRuntime jsr

<PageTitle>Login</PageTitle>

<h1>Login</h1>

@if (!_isAuthenticated)
{
    <table>
        <tr>
            <td>Login:</td>
            <td><input Id="idLogin" @bind="_login" /></td>
        </tr>
        <tr>
            <td>Password:</td>
            <td><input Id="idPassword" @bind="_password" /></td>
        </tr>
        <tr>
            <td><button class="btn btn-primary" @onclick="_submitCredentials">Login</button></td>
        </tr>
    </table>
}
else
{
    <p>Welcome, @_username! You are already authenticated.</p>
}

@code {
    string _login = "";
    string _password = "";
    private bool _isAuthenticated = false;
    private string _username = "";
    DateTime _expiry = DateTime.MinValue;

    protected override async Task OnInitializedAsync()
    {
        var jwtString = await jsr.InvokeAsync<string>("localStorage.getItem", "jwtToken");

        if (jwtString == null)
        {
            return;
        }

        _setAuthenticateState(jwtString);
    }

    private async Task _setAuthenticateState(string? jwtString)
    {
        // Deserialize the JWT string to a JWT token

        var jwtToken = new JwtSecurityTokenHandler().ReadToken(jwtString);
        var token = jwtToken as JwtSecurityToken;

        var expClaim = token.Claims.FirstOrDefault(c => c.Type == "exp");
        long expValue = Convert.ToInt64(expClaim.Value);
        DateTimeOffset expDate = DateTimeOffset.FromUnixTimeSeconds(expValue);
        _expiry = expDate.UtcDateTime;
        _isAuthenticated = DateTime.Now < _expiry;
    }

    private async Task _submitCredentials()
    {
        var credential = new Credential()
            {
                Login = _login,
                Password = _password
            };
        await Http.PutAsJsonAsync("api/login", credential);

        using (var msg = await Http.PutAsJsonAsync("api/login", credential))
        {
            if (msg.IsSuccessStatusCode)
            {
                try
                {
                    LoginResult result = await msg.Content.ReadFromJsonAsync<LoginResult>();

                    if (result.success)
                    {
                        // Save the JWT string to the local storage
                        await jsr.InvokeVoidAsync("localStorage.setItem", "jwtToken", result.jwtBearer).ConfigureAwait(false);
                        await _setAuthenticateState(result.jwtBearer);
                    }
                }
                catch(Exception ex)
                {

                }
            }
        }
    }
}

The protected page

Next, we are going to implement a simple frontend page that uses the BlockController API which is protected by authorization.

Let’s call this frontend page Blocks.razor:

But before we look into the implementation of this page, we make a few changes to our BlockController class, which is the class whose API methods are supposed to be protected by authorization. These changes allow us to be a bit more specific about the data that the API methods return.

BlockController.cs:

using CollaBlocks.Shared;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace CollaBlocks.Server.Controllers
{

    [Route("api/[controller]")]
    [ApiController]
    public class BlockController : ControllerBase
    {
        // Show the admin panel if user has Admin role
        [HttpGet]
        [Route("AdminPanel")]
        [Authorize(Roles = "Admin")]
        public ActionResult<Block> AdminPanel()
        {
            var adminBlock = new Block { Type = "Text", Content = "This info is available to admins of this page." };
            return Ok(adminBlock);
        }

        // Show the user panel if user has User role
        [HttpGet]
        [Route("UserPanel")]
        [Authorize(Roles = "Admin, User")]
        public ActionResult<Block> UserPanel()
        {
            var adminBlock = new Block { Type = "Text", Content = "This info is available to valid users of this page." };
            return Ok(adminBlock);
        }
    }
}

Add the following content to the new Blocks.razor page:

Blocks.razor:

@page "/blocks"
@using CollaBlocks.Shared
@using System
@using System.IdentityModel.Tokens.Jwt
@using System.Security.Claims
@using System.Text
@using Microsoft.IdentityModel.Tokens
@using System.Net.Http
@using System.Net.Http.Headers
@inject HttpClient Http
@inject IJSRuntime jsr
@inject NavigationManager NavigationManager

<PageTitle>Blocks</PageTitle>

<h1>Blocks</h1>

<p></p>
@_publicInfo
<p></p>
@_adminInfo
<p></p>
@_userInfo

@code {
    string _publicInfo = "This info is available to any visitor of this page.";
    string _adminInfo = "";
    string _userInfo = "";
    private bool _isAuthenticated = false;
    DateTime _expiry = DateTime.MinValue;

    protected override async Task OnInitializedAsync()
    {
        var token = await jsr.InvokeAsync<string>("localStorage.getItem", "jwtToken");

        if (token == null)
        {
            NavigationManager.NavigateTo("/login");
        }

        var requestMsg = new HttpRequestMessage(HttpMethod.Get, "api/block/AdminPanel");
        requestMsg.Headers.Add("Authorization", "Bearer " + token);
        var response = await Http.SendAsync(requestMsg);
        if (response.IsSuccessStatusCode)
        {
            // Do something with the response
            var block = await response.Content.ReadFromJsonAsync<Block>();
            _adminInfo = block.Content;
        }

        requestMsg = new HttpRequestMessage(HttpMethod.Get, "api/block/UserPanel");
        requestMsg.Headers.Add("Authorization", "Bearer " + token);
        response = await Http.SendAsync(requestMsg);
        if (response.IsSuccessStatusCode)
        {
            // Do something with the response
            var block = await response.Content.ReadAsAsync<Block>();
            _userInfo = block.Content;
        }
    }
}

This component shows some information to visitors depending on their role. Visitors with the role “admin” see all three texts (@_publicInfo, @_adminInfo, and @_userInfo), visitors with the role “user” see two texts (@_publicInfo and @_userInfo), and visitors without any ‘role’ (basically anonymous visitors) see only the @_publicInfo text.

When the page is loaded for the first time, the OnInitializedAsync() method is called. The token, which was previously saved by the login process handled by login.razor page is loaded from the local storage.

If no token can be found in the local storage the visitor is redirected to the login page.

If a token can be found a new GET HTTP request for the API api/block/AdminPanel is prepared and the token is added to its header. This request is then sent to API.

If the response is successful the data is read from the response and assigned to _adminInfo variable to be shown on the page.

Then the same steps are repeated for the User role.

Troubleshooting

Error: socket hang up

Problem

Trying to access the Web API with Postman we get the following error:

Error: socket hang up

Potential Cause

We have tried to access the HTTPS port (see launchSettings.json file of the Web API Server) using the HTTP protocol:

http://localhost:7190/api/login

launchSettings.json:

{
...
    "profiles": {
      "CollaBlocks.Server": {
        "commandName": "Project",
        "dotnetRunMessages": true,
        "launchBrowser": true,
        "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
        "applicationUrl": "https://localhost:7190;http://localhost:5190",
        "environmentVariables": {
          "ASPNETCORE_ENVIRONMENT": "Development"
        }
      },
...
  }

Potential Resolution

Don’t mix up HTTP protocol with HTTPS port number.

Error: connect ECONNREFUSED 127.0.0.1:5190

Problem

Trying to send a request from Postman request to the API results in Postman issuing the following error:

Error: connect ECONNREFUSED 127.0.0.1:5190

Potential Resolution

Make sure that the API service is actually running.