English
Français

Blog of Denis VOITURON

for a better .NET world

Blazor - Authentication using Cookie

Posted on 2021-08-26

To authenticate a user, Blazor Server uses the same components as ASP.NET Core.

The principle is to inject the service services.AddAuthentication().AddCookie() and call the HttpContext.SignInAsync method, specifying the appropriate Claims.

But the main problem is that Blazor Server uses SignalR to communicate between the web browser and the server. This prevents the correct transfer of cookies. It is therefore necessary to define WebAPIs and to call them using Http.

Global Schema

To help you understand the development of these different steps, I have recorded a video that shows the creation of a Blazor Server project and the integration of all the steps to secure a page or components.

Video - How to create an authenticated Blazor Server project

The complete source code is available here.

1. Creation of the WebAPI

As mentioned in the introduction, Blazor Server cannot send the cookie via SignalR, its usual communication protocol for exchanging its state changes between the browser and the server.

It is therefore necessary to create a WebAPI api/auth/signin to connect (which takes an Email/Password object as argument) and another one to disconnect api/auth/signout.

It is also possible to create GET APIs but the login, and especially the password (even hashed) will be sent in the navigation URL. Personally, I prefer to define APIs of type POST to avoid this.

See the Microsoft’ documentation: Create an authentication cookie

[ApiController]
public class AuthController : ControllerBase
{
    private static readonly AuthenticationProperties COOKIE_EXPIRES = new AuthenticationProperties()
    {
        ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
        IsPersistent = true,
    };

    [HttpPost]
    [Route("api/auth/signin")]
    public async Task<ActionResult> SignInPost(SigninData value)
    {
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Email, value.Email),
            new Claim(ClaimTypes.Name,  value.Email),
            new Claim(ClaimTypes.Role,  "Administrator"),
        };

        var claimsIdentity = new ClaimsIdentity(claims, 
                                                CookieAuthenticationDefaults.AuthenticationScheme);
        var authProperties = COOKIE_EXPIRES;

        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
                                      new ClaimsPrincipal(claimsIdentity),
                                      authProperties);

        return this.Ok();
    }

    [HttpPost]
    [Route("api/auth/signout")]
    public async Task<ActionResult> SignOutPost()
    {
        await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        return this.Ok();
    }
}

public class SigninData
{
    public string Email { get; set; }
    public string Password { get; set; }
}

2. JavaScript code to call the APIs

This second step consists in adding a JS script to call in HTTPS (and not in SignalR) the APIs we have just written.

This auth.js file is placed in the wwwroot/js folder to be accessible by the Blazor engine when the application is executed. The functions are prefixed with export to allow them to be used as JavaScript isolation modules.

export function SignIn(email, password, redirect) {

    var url = "/api/auth/signin";
    var xhr = new XMLHttpRequest();

    // Initialization
    xhr.open("POST", url);
    xhr.setRequestHeader("Accept", "application/json");
    xhr.setRequestHeader("Content-Type", "application/json");

    // Catch response
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) // 4=DONE 
        {
            console.log("Call '" + url + "'. Status " + xhr.status);
            if (redirect)
                location.replace(redirect);
        }
    };

    // Data to send
    var data = {
        email: email,
        password: password
    };

    // Call API
    xhr.send(JSON.stringify(data));
}

export function SignOut(redirect) {

    var url = "/api/auth/signout";
    var xhr = new XMLHttpRequest();

    // Initialization
    xhr.open("POST", url);
    xhr.setRequestHeader("Accept", "application/json");
    xhr.setRequestHeader("Content-Type", "application/json");

    // Catch response
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) // 4=DONE 
        {
            console.log("Call '" + url + "'. Status " + xhr.status);
            if (redirect)
                location.replace(redirect);
        }
    };

    // Call API
    xhr.send();
}

3. Connecting and disconnecting

Now that the WebAPIs and the associated JavaScript code are developed we can use them in our page.

In this example, we modify the Counter page to place two buttons (Login and Logout) and to call the two JavaScript methods.

To do this, we need to inject the JSRunTime. The line var authModule = ... downloads the specified JavaScript file and put it in memory in the component to use its methods.

@page "/counter"
@inject IJSRuntime JSRunTime

<button @onclick="btnLogin_Click">Login</button>
<button @onclick="btnLogout_Click">Logout</button>

@code {

    private async void btnLogin_Click()
    {
        var authModule = await JSRunTime.InvokeAsync<IJSObjectReference>("import", "./js/auth.js");
        await authModule.InvokeVoidAsync("SignIn", "denis@voituron.net", "MyPassword", "/");
    }

    private async void btnLogout_Click()
    {
        var authModule = await JSRunTime.InvokeAsync<IJSObjectReference>("import", "./js/auth.js");
        await authModule.InvokeVoidAsync("SignOut", "/");
    }
}

4. Injection of authentication services

We still have to inject the Cookie authentication services to be able to use it in our application.

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // ... Existing services ...

    services.AddControllers();
    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                options.Cookie.Name = "myauth";
                options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
            });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ... Existing services ...

    app.UseAuthentication();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });
}
<!-- App.razor -->
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <Found Context="routeData">
            <!-- <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> -->
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    <h1>Sorry</h1>
                    <p>You're not authorized to reach this page.</p>
                    <p>You may need to log in as a different user.</p>
                </NotAuthorized>
                <Authorizing>
                    <h1>Authorization in progress</h1>
                    <p>Only visible while authorization is in progress.</p>
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

5. Securing pages and components

A. Now secure your pages by adding the attribute [Authorize] (possibly specifying access roles).

If the user is not known, he will receive the message contained in the App.razor page.

@page "/fetchdata"
@attribute [Authorize]

<h1>Weather forecast</h1>

B. You can also secure your components with the <AuthorizeView> tag.

<AuthorizeView>
    <h3>Hello World @(context.User.Identity.Name)</h3>
</AuthorizeView>

6. (optional) Validation customization

If necessary, it is also possible to capture validation events from authentication cookies. This makes it very easy to inject a data access service (e.g. Factory in this example) to connect to a database and guarantee that the user still exists correctly, with the right access roles.

public class CookieAuthenticationEvents : Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents
{
    //private readonly Factory _factory;

    //public CookieAuthenticationEvents(Factory factory)
    //{
    //    _factory = factory;
    //}

    public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
    {
        // Validate the Principal
        var userPrincipal = context.Principal;

        // If not valid, Sign out
        if (userPrincipal.Identity.Name != "denis@voituron.net")
        {
            context.RejectPrincipal();
            await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
}
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
        .AddCookie(options =>
        {
            options.Cookie.Name = "myauth";
            options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
            // Add this new line
            options.EventsType = typeof(CookieAuthenticationEvents);    // <---
        });
// Add this new line
services.AddScoped<CookieAuthenticationEvents>();   // <---

The complete source code is available here.

Languages

EnglishEnglish
FrenchFrançais

Follow me

Recent posts