Securing Blazor Web assembly using cookies

The article shows how a Blazor web assembly UI hosted in an ASP.NET Core application can be secured using cookies. Azure AD is used as the identity provider and the Microsoft.Identity.Web Nuget package is used to secure the trusted server rendered application. The API calls are protected using the secure cookie and anti-forgery tokens to protected against CSRF. This architecture is also known as the Backends for Frontends (BFF) Pattern.

Code: Blazor Cookie security

Why Cookies

By using cookies, it gives us the possiblity to increase the security of the whole application, UI + API. Blazor web assembly is treated as a UI in the server rendered application. By using cookies, no access tokens, refresh tokens or id tokens are saved or managed in the browser. All security is implemented in the trusted backend. By implementing the security in the trusted backend, the application can be authenticated by the identity provider and all access tokens are removed from the browser, web storage. With the correct security definitions on the cookies, the security risks can be reduced, and the client application can be authenticated. It would be possible to use sender constrained tokens, or Mutual TLS for increased security, if this was required. Anti-forgery tokens are required to secure the API requests because we use cookies.

The UI and the backend are one application which are coupled together. This is different to the standard Blazor template which uses access tokens. The WASM and the API are secured as two separate applications. Here only a single server rendered application is secured. The WASM client can only use APIs hosted on the same domain.

History

2021-03-09 Updated Anti-forgery policy, feedback Philippe De Ryck

Credits

Some of the code in this repo was built using original source code from Bernd Hirschmann.

Thank you for the git repository.

Creating the Blazor application

The Blazor application was created using a web assembly template hosted in an ASP.NET Core application. You need to check the checkbox in Visual Studio for this. No authentication was added. This creates three projects. We will add the security first, then the services to use the Identity of the authenticated user in the WASM client, and then add the bits required for CSRF protection.

Securing the application using Azure AD

The application is secured using the Azure AD identity provider. This is implemented using the Microsoft.Identity.Web web application client, not API client. This is just a wrapper for the Open ID connect code flow authentication and if successful authenticated, the auth data is stored in a cookie. Two Azure App Registrations are used to implement this, one for the API and one for the Web authentication. A client secret is required to access the API. A certificate could also be used instead. See the Microsoft.Identity.Web docs for more info.

The app.settings contains the configuration for both the API and the web client. The ScopeForAccessToken contains all the scopes required by the application so that after the user authenticates, the user can give consent up front for all required APIs. The rest is standard.

"AzureAd": {
	"Instance": "https://login.microsoftonline.com/",
	"Domain": "damienbodhotmail.onmicrosoft.com",
	"TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
	"ClientId": "46d2f651-813a-4b5c-8a43-63abcb4f692c",
	"CallbackPath": "/signin-oidc",
	"SignedOutCallbackPath ": "/signout-callback-oidc"
	// "ClientSecret": "add secret to the user secrets"
},
	"UserApiOne": {
	"ScopeForAccessToken": "api://b2a09168-54e2-4bc4-af92-a710a64ef1fa/access_as_user User.ReadBasic.All user.read",
	"ApiBaseAddress": "https://localhost:44395"
},

The following nuget packages were added to the server blazor host application.

  • Microsoft.AspNetCore.Components.WebAssembly.Server
  • Microsoft.AspNetCore.Authentication.JwtBearer
  • Microsoft.AspNetCore.Authentication.OpenIdConnect
  • Microsoft.Identity.Web
  • Microsoft.Identity.Web.UI
  • Microsoft.Identity.Web.MicrosoftGraphBeta
  • IdentityModel
  • IdentityModel.AspNetCore

The startup ConfigureServices method is used to add the Azure AD authentication clients. The AddMicrosoftIdentityWebAppAuthentication method is used to add the web client which uses the services added in the AddMicrosoftIdentityUI method. Graph API is added as a downstream API demo.

public void ConfigureServices(IServiceCollection services)
{
    // + ...
	
	services.AddHttpClient();
	services.AddOptions();

	string[] initialScopes = Configuration.GetValue<string>(
		"UserApiOne:ScopeForAccessToken")?.Split(' ');

	services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
		.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
		 .AddMicrosoftGraph("https://graph.microsoft.com/beta",
			"User.ReadBasic.All user.read")
		.AddInMemoryTokenCaches();

	services.AddRazorPages().AddMvcOptions(options =>
	{
		var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
			.Build();
		options.Filters.Add(new AuthorizeFilter(policy));
	}).AddMicrosoftIdentityUI();
}

The Configure method is used to add the middleware in the correct order. The Blazor application is setup like the template except for the fallback which maps to the razor page _Host instead of the index. This was added to support anti forgery tokens which I’ll explain later in this blog.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
		app.UseWebAssemblyDebugging();
	}
	else
	{
		app.UseExceptionHandler("/Error");
		app.UseHsts();
	}

	app.UseHttpsRedirection();
	app.UseBlazorFrameworkFiles();
	app.UseStaticFiles();

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

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapRazorPages();
		endpoints.MapControllers();
		endpoints.MapFallbackToPage("/_Host");
	});
}

Now the APIs can be protected using the Authorize attribute with the cookie scheme. The AuthorizeForScopes which come from the Microsoft.Identity.Web Nuget package can be used to validate the scope and handle MSAL consent exceptions.

[ValidateAntiForgeryToken]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[AuthorizeForScopes(Scopes = new string[] { "api://b2a09168-54e2-4bc4-af92-a710a64ef1fa/access_as_user" })]
[ApiController]
[Route("api/[controller]")]
public class DirectApiController : ControllerBase
{
	[HttpGet]
	public IEnumerable<string> Get()
	{
		return new List<string> { "some data", "more data", "loads of data" };
	}
}

Using the claims, identity in the web assembly client application

The next part of the code was implemented using the source code created by Bernd Hirschmann. Now that the server authentication is implemented and the identity exists for the user and the application, the claims from this identity and the state of the actual user needs to be accessed and used in the client web assembly part of the application. APIs need to be created for this purpose. The account controller is used to initialize the sign in flow and a HTTP Post can be used to sign out.

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BlazorAzureADWithApis.Server.Controllers
{
    // orig src https://github.com/berhir/BlazorWebAssemblyCookieAuth
    [Route("api/[controller]")]
    public class AccountController : ControllerBase
    {
        [HttpGet("Login")]
        public ActionResult Login(string returnUrl)
        {
            return Challenge(new AuthenticationProperties
            {
                RedirectUri = !string.IsNullOrEmpty(returnUrl) ? returnUrl : "/"
            });
        }

        // [ValidateAntiForgeryToken] // not needed explicitly due the the auto global definition.
        [Authorize]
        [HttpPost("Logout")]
        public IActionResult Logout()
        {
            return SignOut(
                new AuthenticationProperties { RedirectUri = "/" },
                CookieAuthenticationDefaults.AuthenticationScheme,
                OpenIdConnectDefaults.AuthenticationScheme);
        }
    }
}

The UserController is used to for the WASM to get access about the current identity and the claims of this identity.

using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using BlazorAzureADWithApis.Shared.Authorization;
using IdentityModel;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BlazorAzureADWithApis.Server.Controllers
{
    // orig src https://github.com/berhir/BlazorWebAssemblyCookieAuth
    [Route("api/[controller]")]
    [ApiController]
    public class UserController : ControllerBase
    {
        [HttpGet]
        [AllowAnonymous]
        public IActionResult GetCurrentUser()
        {
            return Ok(User.Identity.IsAuthenticated ? CreateUserInfo(User) : UserInfo.Anonymous);
        }

        private UserInfo CreateUserInfo(ClaimsPrincipal claimsPrincipal)
        {
            if (!claimsPrincipal.Identity.IsAuthenticated)
            {
                return UserInfo.Anonymous;
            }

            var userInfo = new UserInfo
            {
                IsAuthenticated = true
            };

            if (claimsPrincipal.Identity is ClaimsIdentity claimsIdentity)
            {
                userInfo.NameClaimType = claimsIdentity.NameClaimType;
                userInfo.RoleClaimType = claimsIdentity.RoleClaimType;
            }
            else
            {
                userInfo.NameClaimType = JwtClaimTypes.Name;
                userInfo.RoleClaimType = JwtClaimTypes.Role;
            }

            if (claimsPrincipal.Claims.Any())
            {
                var claims = new List<ClaimValue>();
                var nameClaims = claimsPrincipal.FindAll(userInfo.NameClaimType);
                foreach (var claim in nameClaims)
                {
                    claims.Add(new ClaimValue(userInfo.NameClaimType, claim.Value));
                }

                // Uncomment this code if you want to send additional claims to the client.
                //foreach (var claim in claimsPrincipal.Claims.Except(nameClaims))
                //{
                //    claims.Add(new ClaimValue(claim.Type, claim.Value));
                //}

                userInfo.Claims = claims;
            }

            return userInfo;
        }
    }
}

In the client project, the services are added in the program file. The HttpClients are added as well as the AuthenticationStateProvider which can be used in the client UI.

using BlazorAzureADWithApis.Client.Services;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace BlazorAzureADWithApis.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.Services.AddOptions();
            builder.Services.AddAuthorizationCore();
            builder.Services.TryAddSingleton<AuthenticationStateProvider, HostAuthenticationStateProvider>();
            builder.Services.TryAddSingleton(sp => (HostAuthenticationStateProvider)sp.GetRequiredService<AuthenticationStateProvider>());
            builder.Services.AddTransient<AuthorizedHandler>();

            builder.Services.AddHttpClient("default", client =>
            {
                client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            });

            builder.Services.AddHttpClient("authorizedClient", client =>
            {
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            }).AddHttpMessageHandler<AuthorizedHandler>();

            builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("default"));

            await builder.Build().RunAsync();
        }
    }
}

The HostAuthenticationStateProvider implements the AuthenticationStateProvider and is used to call the user controller APIs and return the state to the UI.

using BlazorAzureADWithApis.Shared.Authorization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Threading.Tasks;

namespace BlazorAzureADWithApis.Client.Services
{
    // orig src https://github.com/berhir/BlazorWebAssemblyCookieAuth
    public class HostAuthenticationStateProvider : AuthenticationStateProvider
    {
        private static readonly TimeSpan _userCacheRefreshInterval = TimeSpan.FromSeconds(60);

        private const string LogInPath = "api/Account/Login";
        private const string LogOutPath = "api/Account/Logout";

        private readonly NavigationManager _navigation;
        private readonly HttpClient _client;
        private readonly ILogger<HostAuthenticationStateProvider> _logger;

        private DateTimeOffset _userLastCheck = DateTimeOffset.FromUnixTimeSeconds(0);
        private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity());

        public HostAuthenticationStateProvider(NavigationManager navigation, HttpClient client, ILogger<HostAuthenticationStateProvider> logger)
        {
            _navigation = navigation;
            _client = client;
            _logger = logger;
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            return new AuthenticationState(await GetUser(useCache: true));
        }

        public void SignIn(string customReturnUrl = null)
        {
            var returnUrl = customReturnUrl != null ? _navigation.ToAbsoluteUri(customReturnUrl).ToString() : null;
            var encodedReturnUrl = Uri.EscapeDataString(returnUrl ?? _navigation.Uri);
            var logInUrl = _navigation.ToAbsoluteUri($"{LogInPath}?returnUrl={encodedReturnUrl}");
            _navigation.NavigateTo(logInUrl.ToString(), true);
        }

        private async ValueTask<ClaimsPrincipal> GetUser(bool useCache = false)
        {
            var now = DateTimeOffset.Now;
            if (useCache && now < _userLastCheck + _userCacheRefreshInterval)
            {
                _logger.LogDebug("Taking user from cache");
                return _cachedUser;
            }

            _logger.LogDebug("Fetching user");
            _cachedUser = await FetchUser();
            _userLastCheck = now;

            return _cachedUser;
        }

        private async Task<ClaimsPrincipal> FetchUser()
        {
            UserInfo user = null;

            try
            {
                _logger.LogInformation(_client.BaseAddress.ToString());
                user = await _client.GetFromJsonAsync<UserInfo>("api/User");
            }
            catch (Exception exc)
            {
                _logger.LogWarning(exc, "Fetching user failed.");
            }

            if (user == null || !user.IsAuthenticated)
            {
                return new ClaimsPrincipal(new ClaimsIdentity());
            }

            var identity = new ClaimsIdentity(
                nameof(HostAuthenticationStateProvider),
                user.NameClaimType,
                user.RoleClaimType);

            if (user.Claims != null)
            {
                foreach (var claim in user.Claims)
                {
                    identity.AddClaim(new Claim(claim.Type, claim.Value));
                }
            }

            return new ClaimsPrincipal(identity);
        }
    }
}

The AuthorizedHandler implements the DelegatingHandler which can be used to add headers or handle HTTP request logic when the user is authenticated.

using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace BlazorAzureADWithApis.Client.Services
{
    // orig src https://github.com/berhir/BlazorWebAssemblyCookieAuth
    public class AuthorizedHandler : DelegatingHandler
    {
        private readonly HostAuthenticationStateProvider _authenticationStateProvider;

        public AuthorizedHandler(HostAuthenticationStateProvider authenticationStateProvider)
        {
            _authenticationStateProvider = authenticationStateProvider;
        }

        protected override async Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
            HttpResponseMessage responseMessage;
            if (!authState.User.Identity.IsAuthenticated)
            {
                // if user is not authenticated, immediately set response status to 401 Unauthorized
                responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized);
            }
            else
            {
                responseMessage = await base.SendAsync(request, cancellationToken);
            }

            if (responseMessage.StatusCode == HttpStatusCode.Unauthorized)
            {
                // if server returned 401 Unauthorized, redirect to login page
                _authenticationStateProvider.SignIn();
            }

            return responseMessage;
        }
    }
}

Now the AuthorizeView and the Authorized components can be used to hide or display the UI elements depending on the authentication state of the identity.

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <div class="main">
        <div class="top-row px-4 auth">
            <AuthorizeView>
                <Authorized>
                    <strong>Hello, @context.User.Identity.Name!</strong>
                    <form method="post" action="api//Account/Logout">
                        <AntiForgeryTokenInput/>
                        <button class="btn btn-link" type="submit">Sign out</button>
                    </form>
                </Authorized>
                <NotAuthorized>
                    <a href="Account/Login">Log in</a>
                </NotAuthorized>
            </AuthorizeView>

        </div>

        <div class="content px-4">
            @Body
        </div>
    </div>
</div>

For more information on this, see the Microsoft docs or this blog.

Cross-site request forgery CSRF protection

Cross-site request forgery (also known as XSRF or CSRF) is a possible security problem when using cookies. We can protect against this using anti-forgery tokens and will add this to the Blazor application. To support this, we can use a Razor page _Host.cshtml file instead of a static html file. This host page is added to the server project and uses the default div with the id app just like the index.html file from the dotnet template. The index.html can be deleted form the client project. The render-mode is per default WebAssembly. If you copied a _Host file from a server Blazor template, you would have to change this or remove it.

The Anti-forgery token is added at the bottom of the file in the body. A antiForgeryToken.js is also added to the razor Page _Host file. Also make sure the headers match the headers from the index.html which you deleted.

@page "/"
@namespace BlazorAzureADWithApis.Pages
@using BlazorAzureADWithApis.Client
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Blazor AAD Cookie</title>
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazorAzureADWithApis.Client.styles.css" rel="stylesheet" />
    <link href="manifest.json" rel="manifest" />
    <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
</head>
<body>

    <div id="app">
        <!-- Spinner -->
        <div class="spinner d-flex align-items-center justify-content-center" style="position:absolute; width: 100%; height: 100%; background: #d3d3d39c; left: 0; top: 0; border-radius: 10px;">
            <div class="spinner-border text-success" role="status">
                <span class="sr-only">Loading...</span>
            </div>
        </div>
    </div>

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.webassembly.js"></script>
    <script src="antiForgeryToken.js"></script>
    @Html.AntiForgeryToken()
</body>
</html>

The MapFallbackToPage needs to be updated to use the _Host file instead of the static html.

app.UseEndpoints(endpoints =>
{
	endpoints.MapRazorPages();
	endpoints.MapControllers();
	endpoints.MapFallbackToPage("/_Host");
});

The AddAntiforgery is used to add the service for the CSRF protection by using a header named X-XSRF-TOKEN. The AutoValidateAntiforgeryTokenAttribute is added so that all POST, PUT, DELETE Http requests require an anti-forgery token.

public void ConfigureServices(IServiceCollection services)
{
    // + ...
	
	services.AddAntiforgery(options =>
	{
		options.HeaderName = "X-XSRF-TOKEN";
		options.Cookie.Name = "__Host-X-XSRF-TOKEN";
		options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
		options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
	});

	services.AddControllersWithViews(options =>
		options.Filters.Add(
			new AutoValidateAntiforgeryTokenAttribute()));

}

The antiForgeryToken.js Javascript file uses the hidden input created by the _Host Razor Page file and returns this in a function.


function getAntiForgeryToken() {
    var elements = document.getElementsByName('__RequestVerificationToken');
    if (elements.length > 0) {
        return elements[0].value
    }

    console.warn('no anti forgery token found!');
    return null;
}

The Javascript function can be used in any Blazor component now by using the JSRuntime. The anti-forgery token can be added to the X-XSRF-TOKEN HTTP request header which is configured in the server Startup class.

@page "/directapi"
@inject HttpClient Http
@inject IJSRuntime JSRuntime

<h1>Data from Direct API</h1>

@if (apiData == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Data</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var data in apiData)
            {
                <tr>
                    <td>@data</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private string[] apiData;

    protected override async Task OnInitializedAsync()
    {
        var token = await JSRuntime.InvokeAsync<string>("getAntiForgeryToken");

        Http.DefaultRequestHeaders.Add("X-XSRF-TOKEN", token);

        apiData = await Http.GetFromJsonAsync<string[]>("api/DirectApi");
    }

}

If you are using forms directly in the Blazor template, then a custom component which creates a hidden input can be used to add the anti forgery token to the HTTP POST, PUT, DELETE requests. Underneath is a new component called AntiForgeryTokenInput.

@inject IJSRuntime JSRuntime

<input type="hidden" id="__RequestVerificationToken"
       name="__RequestVerificationToken" value="@GetToken()">

@code {

    private string token = "";

    protected override async Task OnInitializedAsync()
    {
        token = await JSRuntime.InvokeAsync<string>("getAntiForgeryToken");
    }

    public string GetToken()
    {
        return token;
    }

}

The AntiForgeryTokenInput can be used directly in the HTML code.

<form method="post" action="api/Account/Logout">
	<AntiForgeryTokenInput/>
	<button class="btn btn-link" type="submit">Sign out</button>
</form>

In the server application, the ValidateAntiForgeryToken attribute can be used the force using anti forgery token protection explicitly.

[ValidateAntiForgeryToken] 
[Authorize]
[HttpPost("Logout")]
public IActionResult Logout()
{
	return SignOut(
		new AuthenticationProperties { RedirectUri = "/" },
		CookieAuthenticationDefaults.AuthenticationScheme,
		OpenIdConnectDefaults.AuthenticationScheme);
}

Using cookies with Blazor WASM and ASP.NET Core hosted applications can be used to support the high security flow requirements which are required for certain application deployments. This makes it possible to add extra layers of security just by having a trusted application implement the security parts. The Blazor client application can only use the API deployed on the host in the same domain. Any Open ID Connect provider can be supported in this way, just like a Razor Page application. This makes it easier to support logout requirements by using a OIDC backchannel logout and so on. MTLS and sender constrained tokens can also be supported with this setup. SignalR no longer needs to add the access tokens to the URL of the web sockets as cookies can be used on the same domain.

Would love feedback on further ways of improving this.

Links:

https://github.com/berhir/BlazorWebAssemblyCookieAuth

Secure a Blazor WebAssembly application with cookie authentication

https://docs.microsoft.com/en-us/aspnet/core/blazor/components/prerendering-and-integration?view=aspnetcore-5.0&pivots=webassembly#configuration

https://docs.microsoft.com/en-us/aspnet/core/security/anti-request-forgery

https://docs.microsoft.com/en-us/aspnet/core/blazor/security

Securing Blazor Server App using IdentityServer4

https://github.com/saber-wang/BlazorAppFormTset

https://jonhilton.net/blazor-wasm-prerendering-missing-http-client/

https://andrewlock.net/enabling-prerendering-for-blazor-webassembly-apps/

https://docs.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/additional-scenarios

4 comments

  1. […] Securing Blazor Web assembly using cookies (Damien Bowden) […]

  2. […] Securing Blazor Web assembly using cookies [damienbod.com]Blazor WASM applications can be secured using cookies. Damien Bowden shows us how to do that. […]

  3. […] Securing Blazor Web assembly using Cookies and Azure AD […]

  4. […] Securing Blazor Web assembly using Cookies and Azure AD […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

<span>%d</span> bloggers like this: