Nov 4

Written by: Michael Washington
11/4/2018 1:09 PM  RssIcon

 

When you create a non-server-side Blazor application you can use a method such as JWT Authentication with Dotnet Core 2.1 and Blazor app. However, server-side Blazor provides options for deeper integration between the ‘client side’ and ‘server side’ code because the ‘client side’ code is processed server-side. With server-side Blazor, we end up using less code, and things are a lot less complex because we can trust that the end-user was not able to alter or hack the ‘client side’ code.

If the ‘client side’ code says a user is who they say they are, we can trust them. Otherwise, we have to always make a ‘round trip’ to the server, before permitting operations or returning sensitive data. The ‘round trip’ is still being made (because the ‘client site’ code is being generated ‘server side’), but, our code structure is now cleaner and more streamlined.

Application Authentication

image

Most business web applications require their users to log into the application.

image

The users enter their username and passwords, that are checked against a membership database.

image

Once authenticated, the application recognizes the user and now has the ability to deliver content securely.

To demonstrate how authentication works on a server-side Blazor application, we will strip authentication down to its most basic elements. We will simply set a cookie then read that cookie in the application.

Once the authentication process of a server-side Blazor application is understood, we can then implement an authentication and membership management system that meets our needs (for example, one that allows users to create and manage their user accounts).

NOTE: This sample code does not check to see if a person is using a legitimate username and password! You would need to add the proper code to check. This code is just a demonstration of how the process of authorizing a user works.

Create The Application

image

Open Visual Studio 2017.

image

Select File then New then Project.

image

Select ASP.NET Core Web Application.

Name the project BlazorCookieAuth.

image

Select Server-side Blazor.

Add Nuget Packages

image

In the Solution Explorer, right-click on the client project (BlazorCookieAuth.App) and select Manage NuGet Packages.

image

Add references to the following libraries:

  • Microsoft.AspNetCore.Authorization
  • Microsoft.AspNetCore.Http
  • Microsoft.AspNetCore.Identity

Add Cookie Authentication

image

In the server project (BlazorCookieAuth.Server), open the Startup.cs file.

Add the following using statements to the top of the file:

 

// ******
// BLAZOR COOKIE Auth Code (begin)
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using System.Net.Http;
// BLAZOR COOKIE Auth Code (end)
// ******

 

Alter the remaining code to the following, adding the sections marked BLAZOR COOKIE Auth Code:

 

   public class Startup
    {
        // This method gets called by the runtime. Use this method to 
        // add services to the container.
        // For more information on how to configure your application, 
        // visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            // ******
            // BLAZOR COOKIE Auth Code (begin)
            services.Configure<CookiePolicyOptions>(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });
            services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie();
            // BLAZOR COOKIE Auth Code (end)
            // ******
            // Adds the Server-Side Blazor services, and those registered 
            // by the app project's startup.
            services.AddServerSideBlazor<App.Startup>();
            // ******
            // BLAZOR COOKIE Auth Code (begin)
            // From: https://github.com/aspnet/Blazor/issues/1554
            // HttpContextAccessor
            services.AddHttpContextAccessor();
            services.AddScoped<HttpContextAccessor>();
            services.AddHttpClient();
            services.AddScoped<HttpClient>();
            // BLAZOR COOKIE Auth Code (end)
            // ******
            services.AddResponseCompression(options =>
            {
                options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
                {
                    MediaTypeNames.Application.Octet,
                    WasmMediaTypeNames.Application.Wasm,
                });
            });
        }
        // This method gets called by the runtime. 
        // Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseResponseCompression();
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            // ******
            // BLAZOR COOKIE Auth Code (begin)
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();
            app.UseAuthentication();
            app.UseMvc(routes =>
            {
                routes.MapRoute(name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
            // BLAZOR COOKIE Auth Code (end)
            // ******
            // Use component registrations and static files from the app project.
            app.UseServerSideBlazor<App.Startup>();
        }
    }

 

First the code adds support for cookies. Cookies are created by the application, and passed to the user’s web browser when the user logs in. The web browser passes the cookie back to the application to indicate that the user is authenticated. When the user ‘logs out’, the cookie is removed.

This code also adds:

  • HttpContextAccessor
  • HttpClient

as services that will be accessed in client-side code using dependency Injection.

See this link for a full explanation of how HttpContextAccessor allows us to determine who the logged in user is.

The code also adds routing so that application navigation, from the client project Blazor code, to the server project login and logout pages, will work.

 

Add Server Project Login/Logout Pages

image

Logging in (and out) is performed in the server project.

Add a folder called Pages and add the following Razor pages and code:

 

_ViewImports.cshtml

@using Microsoft.AspNetCore.Identity
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

 

Login.cshtml

 

@page
@model BlazorCookieAuth.Server.Pages.LoginModel
@{
    ViewData["Title"] = "Log in";
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width">
    <title>BlazorCookieAuth</title>
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/site.css" rel="stylesheet" />
</head>
<body>
    <app>
        <div class="sidebar">
            <div class="top-row pl-4 navbar navbar-dark">
                <a class="navbar-brand" href="">BlazorCookieAuth</a>
            </div>
        </div>
        <div class="main">
            <div class="top-row px-4">
                <h2>@ViewData["Title"]</h2>
            </div>
            <div class="content px-4">
                <form id="account" method="post">
                    <div class="form-group">
                        <label asp-for="Input.Email"></label>
                        <input asp-for="Input.Email" class="form-control" />
                    </div>
                    <div class="form-group">
                        <label asp-for="Input.Password"></label>
                        <input asp-for="Input.Password" class="form-control" />
                    </div>
                    <div class="form-group">
                        <button type="submit" class="btn btn-primary">Log in</button>
                    </div>
                </form>
            </div>
        </div>
    </app>
</body>
</html>

 

Login.cshtml.cs

 

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace BlazorCookieAuth.Server.Pages
{
    [AllowAnonymous]
    public class LoginModel : PageModel
    {
        public string ReturnUrl { get; set; }
        [BindProperty]
        public InputModel Input { get; set; }
        public class InputModel
        {
            [Required]
            [EmailAddress]
            public string Email { get; set; }
            [Required]
            [DataType(DataType.Password)]
            public string Password { get; set; }
        }
        public async Task OnGetAsync(string returnUrl = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
            // Clear the existing external cookie
            try
            {
                await HttpContext
                    .SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            }
            catch { }
            ReturnUrl = returnUrl;
        }
        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
            // *** !!! This is where you would validate the user !!! ***
            // In this example we just log the user in
            // (Always log the user in for this demo)
            var claims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, Input.Email),
                    new Claim(ClaimTypes.Role, "Administrator"),
                };
            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);
            var authProperties = new AuthenticationProperties
            {
                IsPersistent = true,
                RedirectUri = this.Request.Host.Value
            };
            try
            {
                await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(claimsIdentity),
                authProperties);
                return LocalRedirect(returnUrl);
            }
            catch 
            {
                return Page();
            }
        }
    }
}

 

Logout.cshtml

 

@page
@model BlazorCookieAuth.Server.Pages.LogoutModel
@{
    ViewData["Title"] = "Logout";
}
<h2>Logout</h2>

 

Logout.cshtml.cs

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace BlazorCookieAuth.Server.Pages
{
    public class LogoutModel : PageModel
    {
        public string ReturnUrl { get; private set; }
        public async Task<IActionResult> OnGetAsync(string returnUrl = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");
            // Clear the existing external cookie
            try
            {
                await HttpContext
                    .SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            }
            catch (Exception ex)
            {
                string error = ex.Message;
            }
            return LocalRedirect(returnUrl);
        }
    }
}

 

Add Client Project Code

image

In the client project, add a page called Login.cshtml to the Pages folder using the following code:

 

@using System.Security.Claims
@using Microsoft.AspNetCore.Http
@using BlazorCookieAuth.App.Shared
@page "/login"
@inject IHttpContextAccessor _httpContextAccessor
@inject HttpClient Http
    @if (User.Identity.Name != null)
    {
        <b>You are logged in as: @User.Identity.Name</b> 
        <a class="ml-md-auto btn btn-primary" 
           href="/logout?returnUrl=/" 
           target="_top">Logout</a>
    }
    else
    {
        <a class="ml-md-auto btn btn-primary" 
           href="/login?returnUrl=/" 
           target="_top">Login</a>
    }
    @functions {
        private ClaimsPrincipal User;
        protected override void OnInit()
        {
            base.OnInit();
            try
            {
                // Set the user to determine if they are logged in
                User = _httpContextAccessor.HttpContext.User;
            }
            catch { }
        }
    }

 

This code creates a login component (because the @page tag is set to ‘/login’), that calls the IHttpContextAccessor (that was registered in the server project code using dependency injection), to determine if the user is logged in and that they have a “Name”.

If the user is logged in, we display their name and a Logout button (that navigates the user to the logout page in the server project).

If they are not logged in, we display a Login button (that navigates the user to the login page in the server project).

 

image

Finally, we alter the MainLayout.cshtml page (in the Shared folder) to the following:

 

@inherits BlazorLayoutComponent
<div class="sidebar">
    <NavMenu />
</div>
<div class="main">
    <div class="top-row px-4">
        <!-- BLAZOR COOKIE Auth Code (begin) -->
        <Login />
        <!-- BLAZOR COOKIE Auth Code (end) -->
    </div>
    <div class="content px-4">
        @Body
    </div>
</div>

 

This adds the login component to the top of every page in the Blazor application.

image

We can now hit F5 to run the application.

image

We can click the Login button…

image

Log in

image

We can then look in the Google Chrome Web Browser DevTools and see the cookie has been created.

image

When we click Logout

image

The cookie is removed.

 

Special Thanks

This article would not be possible without sample code (using the full .Net Core Membership provider) provided by SQL-MisterMagoo.

 

Links

Blazor.net

BlazorTest (SQL-MisterMagoo's site)

Authentication for serverside Blazor (How to use IHttpContextAccessor)

Blazor Security/Authorization

Use cookie authentication without ASP.NET Core Identity

JWT Authentication with Dotnet Core 2.1 and Blazor app

Download

The project is available at http://lightswitchhelpwebsite.com/Downloads.aspx

You must have Visual Studio 2017 (or higher) installed to run the code.

19 comment(s) so far...


Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

It doesn't work Michael! In your client side Login page, which you put inside the MainLayout.cshtml component, you have

@if (User.Identity.Name != null)
{
You are logged in as: @User.Identity.Name
Logout
}
else
{
Login
}

However, the link href="/Login?returnUrl=/" doesn't go to server side Login page. You also can try by typing it in the URL directly. I guess it's the MVC's router problem. I suspect you would need to build a controller to get the router to work.

By Jeff on   11/9/2018 4:42 AM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

@Jeff - I have tested this on multiple computers and it works fine. I have the route in the Startup.cs file in the Server project. Did you download the code from the download page on this site and test it? Are you running Blazor version 0.60 (or higher)?

By Michael Washington on   11/9/2018 4:40 AM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

@Jeff - Another user reported the same problem. I looked at his project and it turns out in the .csproj file he was missing PrivateAssets="all" on the PackageReference for "Microsoft.AspNetCore.Blazor.Build" Version="0.6.0"

By Michael Washington on   11/12/2018 5:09 PM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

Hello Michael,

thank you for this tutorial.
I'm new to blazor and I was wondering if this(cookies) is a secure way to do Authentication ?
Second , would this also work with HTTPS enabled ?

Regards and thank you,
Gert

By Gert on   12/8/2018 5:17 AM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

@Gert - Practically every site uses cookies for authentication. Yes, this works with HTTPS.

By Michael Washington on   12/8/2018 5:18 AM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

Thank you very much for your great article as well as the comments. My project create since earlier version of Blazor 2.0, so "Microsoft.AspNetCore.Blazor.Build" is missing the attribute "PrivateAssets" that cause the router in MVC not working. Added it and it' working like a charm. Thank you again and keep up your great work!

By Tony on   12/20/2018 11:53 AM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

Is there a way to pass a string or an object from App project to the Server project ? I tried using context.Item, Local storage, or even cookies

By Longinos on   12/21/2018 2:38 PM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

@Longinos - You can by creating a service that is registered in te startup.cs file. Anything placed in it is avaliable on both the client and server side. I don't have an examples, but you can find one on the blazor.net site

By Michael Washington on   12/21/2018 2:40 PM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

Hello and thank you for this tutorial,

I encountered a problem with httpContextAccessor; after login I can see the cookie but the User in _httpContextAccessor.HttpContext.User is some times Null,
You can reproduce : after login hit F5 "refresh of the browser" many times, you'll see that sometimes the _httpContextAccessor.HttpContext.User is null

Is there any alternative or workaround to pass the User to the App (MainLayout.cshtml here) ?

protected override void OnInit()
{
base.OnInit();
try
{
// Set the user to determine if they are logged in
User = _httpContextAccessor.HttpContext.User;
}
catch { }
}

By (R)ed on   1/14/2019 9:54 AM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

@(R)ed - I have tested this on multiple computers and it works fine. Did you download the code from the download page on this site and test it? Are you running Blazor version 0.60 (or higher)?

By Michael Washington on   1/14/2019 9:55 AM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

Yes, I'm using Blazor 0.7, it works fine on dev environment when using VS debug /IIS Express, I encounter the problem when the app is hosted on a server (ASP.NET Core 2.1 shared Hosting)
I publish using command :
dotnet publish -c release -r win-x86

I already noticed errors on the browser console :
WebSocket connection to 'wss://xxxxxxxx.yyyyy/_blazor?id=HAorR2IG0bLwcKC4DS6TOA' failed: Error during WebSocket handshake: Unexpected response code: 200 blazor.server.js:16

And warnings :
Warning: Timeout from HTTP request. blazor.server.js:16

Maybe its a SignalR issue ? or I missed something when publishing to server (ASP.NET Core 2.1 shared Hosting)

Regards and thank you,

By (R)ed on   1/15/2019 1:07 PM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

@(R)ed - This page on Hosting, on the Blazor site, may help https://blazor.net/docs/host-and-deploy/index.html

By Michael Washington on   1/15/2019 1:14 PM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

Hello Michael,

i don't think, this is the right way to solve the problem, but i think you found a good way for this time. You mix Server and Client Pages and don't use blazor components. I hope we can found a better way.

By Marco on   2/3/2019 1:27 PM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

@Marco - Before Razor Components are released there will be one project not two. This should make the setup make more sense. Also this is only for server side Blazor (Razor Components) NOT client Blazor.

By Michael Washington on   2/3/2019 2:12 PM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

Hi Michael, thanks again for all your hard work.
Couple of questions.

Has this changed for the most current versions of .net core and VS2019?
Could you create a tutorial using similar login/logout forms and backend database that is created when you create a MVC Application using 'Individual User Accounts' authentication. (I know you have nothing better to do :) )

By Dave Vorgang on   2/21/2019 12:27 PM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

@Dave Vorgang - Yes things changed for VS2019. Microsoft is working on something "official" so I would rather wait to see what they come up with. You can track it here: https://github.com/aspnet/AspNetCore/issues/4048

By Michael Washington on   2/21/2019 1:56 PM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

@Michael Washington - Thanks for the info.

By Dave Vorgang on   2/21/2019 1:55 PM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

Hi Michael,
Excellent post! How would you redirect the user to the login page if they were not authenticated? Would that be handled in the startup.cs on the server side?

By Clay Borne on   4/4/2019 3:34 AM
Gravatar

Re: A Demonstration of Simple Server-side Blazor Cookie Authentication

@Clay Borne - See the "Blazor Workshop" ( https://github.com/dotnet-presentations/blazor-workshop/blob/master/docs/05-authentication-and-authorization.md ) for example code.

By Michael Washington on   4/4/2019 3:41 AM

Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 
Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation