The article shows how an ASP.NET Core API and a Blazor BBF application can be implemented in the same project and secured using Azure AD with Microsoft.Identity.Web. The Blazor application is secured using the BFF pattern with its backend APIs protected using cookies with anti-forgery protection and same site. The API is protected using JWT Bearer tokens and used from a separate client from a different domain, not from the Blazor application. This API is not used for the Blazor application. When securing Blazor WASM hosted in an ASP.NET Core application, BFF architecture should be used for the Blazor application and not JWT tokens, especially in Azure where it is not possible to logout correctly.

Code: https://github.com/damienbod/AspNetCore6Experiments

Setup

The Blazor application consists of three projects. The Server project implements the OpenID Connect user interaction flow and authenticates the client as well as the user authentication. The APIs created for the Blazor WASM are protected using cookies. A second API is implemented for separate clients and the API is protected using JWT tokens. Two separate Azure App registrations are setup for the UI client and the API. If using the API, a third Azure App registration would be used for the client, for example an ASP.NET Core Razor page, or a Power App.

API

The API is implemented and protected with the MyJwtApiScheme scheme. This will be implemented later in the Startup class. The API uses swagger configurations for Open API 3 and a simple HTTP GET is implemented to validate the API security.

 [Route("api/[controller]")] [ApiController] [Authorize(AuthenticationSchemes = "MyJwtApiScheme")] [Produces("application/json")] [SwaggerTag("Using to provide a public api for different clients")] public class MyApiJwtProtectedController : ControllerBase { 	[HttpGet] 	[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))] 	[SwaggerOperation(OperationId = "MyApiJwtProtected-Get",  		Summary = "Returns string with details")] 	public IActionResult Get() 	{ 		return Ok("yes my public api protected with Azure AD and JWT works"); 	} } 

Blazor BFF

The Blazor applications are implemented using the backend for frontend security architecture. All security is implemented in the backend and the client requires a secret or a certificate to authenticate. The security data is stored to an encrypted cookie with same site protection. This is easier to secure than storing tokens in the browser storage, especially since Blazor does not support strong CSPs due to the generated Javascript and also that AAD does not support a proper logout for access tokens, refresh tokens stored in the browser. The following blog post explains this in more details.

Securing Blazor Web assembly using cookies

Microsoft.Identity.Web

The Microsoft.Identity.Web Nuget package is used to implement the Azure AD clients. This setup is different to the documentation. The default schemes need to be set correctly when using Cookie (App) authentication and also API Auth together. The AddMicrosoftIdentityWebApp method sets up the Blazor authentication for one Azure App registration using configuration from the AzureAd settings. The AddMicrosoftIdentityWebApi method implements the second Azure App registration for the JWT Bearer token Auth using the AzureAdMyApi settings and the MyJwtApiScheme scheme.

 services.AddAuthentication(options =>  { 	options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 	options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; 	options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; 	options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddMicrosoftIdentityWebApp(Configuration, "AzureAd",  	OpenIdConnectDefaults.AuthenticationScheme)    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)    .AddMicrosoftGraph("https://graph.microsoft.com/beta", 		"User.ReadBasic.All user.read")    .AddInMemoryTokenCaches();  services.AddAuthentication("MyJwtApischeme") 	.AddMicrosoftIdentityWebApi( 		Configuration, "AzureAdMyApi", "MyJwtApiScheme"); 

app.settings

The ASP.NET Core project uses app.settings and user secrets in development to configure the Azure AD clients. The two Azure App registrations values are added here.

 "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" }, "AzureAdMyApi": { 	"Instance": "https://login.microsoftonline.com/", 	"Domain": "damienbodhotmail.onmicrosoft.com", 	"TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1", 	"ClientId": "b2a09168-54e2-4bc4-af92-a710a64ef1fa" }, 

Swagger

Swagger is added to make it easier to view and test the API. A simple UI is created so that you can paste your access token into the UI and test the APIs manually if required. You could also implement a user flow directly in the Swagger UI but then you would have to open up the security headers protection to allow this.

 services.AddSwaggerGen(c => { 	c.EnableAnnotations();  	// add JWT Authentication 	var securityScheme = new OpenApiSecurityScheme 	{ 		Name = "JWT Authentication", 		Description = "Enter JWT Bearer token **_only_**", 		In = ParameterLocation.Header, 		Type = SecuritySchemeType.Http, 		Scheme = "bearer", // must be lower case 		BearerFormat = "JWT", 		Reference = new OpenApiReference 		{ 			Id = JwtBearerDefaults.AuthenticationScheme, 			Type = ReferenceType.SecurityScheme 		} 	}; 	c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme); 	c.AddSecurityRequirement(new OpenApiSecurityRequirement 	{ 		{securityScheme, Array.Empty<string>() } 	}); 	c.SwaggerDoc("v1", new OpenApiInfo 	{ 		Title = "My API", 		Version = "v1", 		Description = "My API" 	});  }); 

The Swagger middleware is added after the security headers middleware. Some people only add this to dev and not production deployments.

 app.UseSwagger();  app.UseSwaggerUI(c => { 	c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyApi v1"); });  

Testing

The UITestClientForApiTest Razor Page application can be used to login and get an access token to test the API. Before starting this application, the AzureAD configuration in the settings need to be updated to match your Azure App registration and your tenant. The access token can be used directly in the Swagger UI. The API only accepts delegated access tokens and no CC tokens etc. The configuration in the Blazor server application also needs to match the Azure App registrations in your tenant.

This setup is good for simple projects where you would like to avoid creating a second deployment or you want to re-use a small amount of business logic from the Blazor server. At some stage, it would probably make sense to split the API and the Blazor UI into two separate projects which would make this security setup more simple again but result in more infrastructure.

Links:

https://github.com/AzureAD/microsoft-identity-web

https://github.com/AzureAD/microsoft-identity-web/wiki/Mixing-web-app-and-web-api-in-the-same-ASP.NET-core-app

Securing Blazor Web assembly using cookies

https://jwt.ms/