Blazor

Building a blogging app with Blazor: Adding Authentication

This post is part 6 of a series, Building a blogging app with Blazor.

Last time I added editing and deleting to the blogging app, this finished off the admin functions. In this final post I'm going to add authentication to protect those admin functions. Let's get started.

The Server

For the purposes of this demo app I'm going to add basic authentication using JSON web tokens. The majority of the server code is inspired by this blog series by Jon Hilton. Just to be clear though, you will need a more robust way of authenticating username and passwords. But as our focus is more on the Blazor side of things, this will be fine.

I'm going to start by adding a appsettings.json file to the project with the following key-value pairs.

{
    "JwtSecurityKey": "RANDOM_KEY_MUST_NOT_BE_SHARED",
    "JwtIssuer": "https://localhost",
    "JwtExpiryInDays": 14
}

Then the following to the Startup.cs

public IConfiguration Configuration { get; }

public Startup(IHostingEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
    Configuration = builder.Build();
}

This code is loading the key value pairs from the appsettings.json into a IConfiguration instance so they are available for use within the app.

Next I'm going to add a LoginController with a Login method, this where I'll submit the username and password to from the Blazor client.

public class LoginController : Controller
{
    private readonly IConfiguration _configuration;
    
    public LoginController(IConfiguration configuration)
    {
        _configuration = configuration;
    }
    
    [HttpPost(Urls.Login)]
    public IActionResult Login([FromBody] LoginDetails login)
    {
        if (login.Username == "admin" && login.Password == "SuperSecretPassword")
        {
            var claims = new[]
            {
                new Claim(ClaimTypes.Name, login.Username)
            };
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var expiry = DateTime.Now.AddDays(Convert.ToInt32(_configuration["JwtExpiryInDays"]));
            var token = new JwtSecurityToken(
                _configuration["JwtIssuer"],
                _configuration["JwtIssuer"],
                claims,
                expires: expiry,
                signingCredentials: creds
            );
            
            return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
        }
        
        return BadRequest("Username and password are invalid.");
    }
}

Once again, hard-coding usernames and passwords this way is not good. In a real app I'd be checking against something like Azure AD or the like.

I'm not going to go into detail about what this code is doing. As I mentioned before, you can read all about it in Jon Hiltons blog. But to summarise, if the username and password match a valid JWT will be generated and returned to the caller. Otherwise, they will receive a 400 Bad Request.

I need to add a couple of items to the shared project, the LoginDetails class and a new login route to the Urls class.

public class LoginDetails
{
    public string Username { get; set; }
    public string Password { get; set; }
}
public const string Login = "api/login";

Back in the Startup.cs I need to enable authentication and specifically JWT bearer authentication. First I'll add the following to the ConfigureServices method above the call to register the MVC services.

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options => 
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = Configuration["JwtIssuer"],
                ValidAudience = Configuration["JwtIssuer"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtSecurityKey"]))
            };
        });

Then I'll add the following to the Configure method just above the app.UseMvc statement.

app.UseAuthentication();

I've now got my app setup to use JWT bearer authentication. All that's left to do is add the [Authorize] attribute above any endpoints I want to require an authorised user. In the BlogPostController that is the AddBlogPost, UpdateBlogPost and DeleteBlogPost methods.

That concludes the changes needed in the server project, now let's move on to the client.

The Client

I'm going to start by adding a nuget package called BlazoredLocalStorage. This is a simple library I built to provide access to the browsers local storage APIs from Blazor. I'm going to be using it to store the JWT that comes from server after a successful login.

Next is the AppState class. This will contain the log in and log out methods as well as maintain if the user is logged in or not. This class will use the library above to save the auth token to local storage.

public class AppState
{
    private readonly HttpClient _httpClient;
    private readonly ILocalStorage _localStorage;

    public bool IsLoggedIn { get; private set; }

    public AppState(HttpClient httpClient,
                    ILocalStorage localStorage)
    {
        _httpClient = httpClient;
        _localStorage = localStorage;
    }

    public async Task Login(LoginDetails loginDetails)
    {
        var response = await _httpClient.PostAsync(Urls.Login, new StringContent(Json.Serialize(loginDetails), Encoding.UTF8, "application/json"));

        if (response.IsSuccessStatusCode)
        {
            await SaveToken(response);
            await SetAuthorizationHeader();

            IsLoggedIn = true;
        }
    }

    public async Task Logout()
    {
        await _localStorage.RemoveItem("authToken");
        IsLoggedIn = false;
    }

    private async Task SaveToken(HttpResponseMessage response)
    {
        var responseContent = await response.Content.ReadAsStringAsync();
        var jwt = Json.Deserialize<JwToken>(responseContent);

        await _localStorage.SetItem("authToken", jwt.Token);
    }

    private async Task SetAuthorizationHeader()
    {
        if (!_httpClient.DefaultRequestHeaders.Contains("Authorization"))
        {
            var token = await _localStorage.GetItem<string>("authToken");
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }
    }
}

The code is pretty straightforward, the main work is being done in the Login method. It posts the login details up to the API, if it's successful then it saves the token to local storage, applies the authorization header to the HttpClient and marks the user as logged in.

I need to register AppState with the DI container. This is done in Startup.cs in the ConfigureServices method. I'll also add the services for BlazoredLocalStorage while I'm here as I forgot to do this earlier.

public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalStorage();
    services.AddSingleton<AppState>();
}

I can now track the logged in state of a user and I've got the ability to perform log in and log out requests. Now I'm going to make some changes to the MainLayout component to make use of some of that functionality.

First, I'm going to add a model class as currently I've only got the component markup defined. Up till now there hasn't been any logic required in this component. But as I've said previously, I prefer to keep the logic separated from the markup as much as possible.

public class MainLayoutModel : BlazorLayoutComponent
{
    [Inject] protected AppState AppState { get; set; }

    protected async Task Logout()
    {
        await AppState.Logout();
    }
}

Notice I'm inheriting from BlazorLayoutComponent here not the usual BlazorComponent. This is because BlazorLayoutComponent exposes a RenderFragment called Body which holds the content to be rendered in the layout.

Now I have access to AppState and I have a log out method on my layout component. I'm going to make some changes to the main menu.

<ul class="navbar-nav ml-auto">
    <li class="nav-item">
        <NavLink href="/" Match="NavLinkMatch.All">Home</NavLink>
    </li>
    @if (AppState.IsLoggedIn)
    {
    <li class="nav-item">
        <NavLink href="/addpost">Add Post</NavLink>
    </li>
    <li class="nav-item">
        <button class="logout" onclick=@Logout>Log Out</button>
    </li>
    }
    else
    {
    <li class="nav-item">
        <NavLink href="/login">Log In</NavLink>
    </li>
    }
</ul>

As you can see I've added a check to see if the user is logged in, if they are then I'm showing the link to add a post. I'm also showing a button which allows the user to log out if they wish.

If the user isn't logged in then I'm showing a log in link which will send them to the log in component I'm going to build next.

I'm going to add a new folder called Login to the Features folder. Then add the following class, Login.cshtml.cs.

public class LoginModel : BlazorComponent
{
    [Inject] private AppState _appState { get; set; }
    [Inject] private IUriHelper _uriHelper { get; set; }
    
    protected LoginDetails LoginDetails { get; set; } = new LoginDetails();
    protected bool ShowLoginFailed { get; set; }

    protected async Task Login()
    {
        await _appState.Login(LoginDetails);

        if (_appState.IsLoggedIn)
        {
            _uriHelper.NavigateTo("/");
        }
        else
        {
            ShowLoginFailed = true;
        }
    }
}

Then I'm going to add the component markup, Login.cshtml.

@page "/login" 

@layout MainLayout
@inherits LoginModel

<WdHeader Heading="WordDaze" SubHeading="Please Enter Your Login Details"></WdHeader>

<div class="container">
    <div class="row">
        <div class="col-md-4 offset-md-4">
            <div class="editor">
                @if (ShowLoginFailed)
                {
                    <div class="alert alert-danger">
                        Login attempt failed.
                    </div>
                }
                <input type="text" bind=@LoginDetails.Username placeholder="Username" class="form-control" />
                <input type="password" bind=@LoginDetails.Password placeholder="Username" class="form-control" />
                <button class="btn btn-primary" onclick="@Login">Login</button>
            </div>
        </div>
    </div>
</div>

The component renders a simple form with username, password inputs. When a user attempts to login a call is made to the Login method on the AppState class. Once this is completed the IsLoggedIn property is checked on AppState. If this is true then the user is redirected to the home page and will now see the links available to authenticated users. If it's false, then a message is displayed stating the login attempt failed.

At this point things are looking pretty good! You should be able to fire up the app and log in, submit a post as well as be able to edit and delete it. Pretty cool, but there are a couple of little things I want to tidy up before I call it a day.

Currently a non-authenticated user can go directly to the add post component and while they will not be able to post anything this feels a bit clunky. I can check if the user is logged in in the OnInitAsync method. If they're not then I can simply redirect them back to the home page. Which should work nicely.

[Inject] private AppState _appState { get; set; }

protected override async Task OnInitAsync()
{
    if (!_appState.IsLoggedIn) 
    {
        _uriHelper.NavigateTo("/");
    }

    if (!string.IsNullOrEmpty(PostId))
    {
        await LoadPost();
    }
}

That's much better. The only other issue I have is the hard-coded author name. Now I have an authenticated user it would be good to use that name on posts. So I'm going to removed the hard-coded value from SavePost method on the PostEditorModel class. Then back in the server project I'm going to update the AddBlogPost method on the BlogPostsController to the following.

[Authorize]
[HttpPost(Urls.AddBlogPost)]
public IActionResult AddBlogPost([FromBody]BlogPost newBlogPost)
{
    newBlogPost.Author = Request.HttpContext.User.Identity.Name;
    var savedBlogPost = _blogPostService.AddBlogPost(newBlogPost);

    return Created(new Uri(Urls.BlogPost.Replace("{id}", savedBlogPost.Id.ToString()), UriKind.Relative), savedBlogPost);
}

All done.

Wrapping up

In this post I've covered adding authorisation to my blogging application. A user can now log in and add new posts and edit or delete existing ones. This has been achieved by the use of JSON web tokens.

This post also marks the end of this series on building a blogging app with Blazor. I hope you've enjoyed reading them and have found something useful. As always, if you have any questions or comment then please let me know below. You can find all the code for this series on my GitHub.