This template can be used to create a Blazor WASM application hosted in an ASP.NET Core Web app using Azure B2C and Microsoft.Identity.

  • By damienbod
  • Last update: Dec 14, 2022
  • Comments: 8

Blazor.BFF.AzureB2C.Template

.NET NuGet Status Change log

This template can be used to create a Blazor WASM application hosted in an ASP.NET Core Web app using Azure B2C and Microsoft.Identity.Web to authenticate using the BFF security architecture. (server authentication) This removes the tokens form the browser and uses cookies with each HTTP request, response. The template also adds the required security headers as best it can for a Blazor application.

Blazor BFF Azure B2C

Features

  • WASM hosted in ASP.NET Core 6
  • BFF with Azure B2C using Microsoft.Identity.Web
  • OAuth2 and OpenID Connect OIDC
  • No tokens in the browser

Other templates

Blazor BFF Azure AD

Blazor BFF Azure OpenID Connect

Using the template

install

dotnet new -i Blazor.BFF.AzureB2C.Template

run

dotnet new blazorbffb2c -n YourCompany.Bff

Use the -n or --name parameter to change the name of the output created. This string is also used to substitute the namespace name in the .cs file for the project.

Setup after installation

Add the Azure B2C App registration settings

"AzureB2C": {
	"Instance": "https://--your-domain--.b2clogin.com",
	"Domain": "[Enter the domain of your B2C tenant, e.g. contoso.onmicrosoft.com]",
	"TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]",
	"ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]",
	"ClientSecret": "[Copy the client secret added to the app from the Azure portal]",
	"ClientCertificates": [
	],
	// the following is required to handle Continuous Access Evaluation challenges
	"ClientCapabilities": [ "cp1" ],
	"CallbackPath": "/signin-oidc"
	// Add your policy here
	"SignUpSignInPolicyId": "B2C_1_signup_signin", 
	"SignedOutCallbackPath": "/signout-callback-oidc"
},

Add the permissions for Microsoft Graph if required, application scopes are used due to Azure B2C

"GraphApi": {
	// Add the required Graph permissions to the Azure App registration
	"TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]",
	"ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]",
	"Scopes": ".default"
	//"ClientSecret": "--in-user-secrets--"
},

uninstall

dotnet new -u Blazor.BFF.AzureB2C.Template

Development

build

https://docs.microsoft.com/en-us/dotnet/core/tutorials/create-custom-template

nuget pack content/Blazor.BFF.AzureB2C.Template.nuspec

install developement

Locally built nupkg:

dotnet new -i Blazor.BFF.AzureB2C.Template.1.0.7.nupkg

Local folder:

dotnet new -i 
   

   

Where is the path to the folder containing .template.config.

Azure App registrations documentation

https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-register-applications

https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-register-applications?tabs=app-reg-ga

Credits, Used NuGet packages + ASP.NET Core 6.0 standard packages

  • NetEscapades.AspNetCore.SecurityHeaders
  • IdentityModel.AspNetCore

Links

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

Github

https://github.com/damienbod/Blazor.BFF.AzureB2C.Template

Comments(8)

  • 1

    Correlation failed error when deployed to container apps and app service containers - missing cookies, maybe samesite issue?

    Hello @damienbod ,

    I watched your recent stand-up with Jon Galloway which was super interesting and I thought I'd give this BFF template a go. Locally it works great, and deployed to a standard app service plan it works fine too. However, I'm pulling my hair out trying to get it to work with Azure container apps and was hoping you could point me in the right direction to fix this. Please note, before the index page would work I had to I fix another issue with the anti-forgery cookie which was resolved with the following change:

    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;

    to

    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;

    Once the above was resolved the index page shows fine but I unfortunately get the correlation failed error reported below when selecting the login link. Any help with this is greatly appreciated:

    Issue

    Correlation failed error reported when selecting the log-in link on the main page. The container app logs show the following (newest first):

    [40m[1m[33mwarn[39m[22m[49m: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[15]

    '.AspNetCore.Correlation.JV4ZtwsqyoQzUm0XjKPsZl5UeG7IAsHK18V9J3fEtwU' cookie not found.

    [40m[1m[33mwarn[39m[22m[49m: Microsoft.AspNetCore.Http.ResponseCookies[1]

    The cookie '.AspNetCore.Correlation.JV4ZtwsqyoQzUm0XjKPsZl5UeG7IAsHK18V9J3fEtwU' has set 'SameSite=None' and must also set 'Secure'.

    [40m[1m[33mwarn[39m[22m[49m: Microsoft.AspNetCore.Http.ResponseCookies[1]

    The cookie '.AspNetCore.OpenIdConnect.Nonce.CfDJ8P3OW6sbpfpEkZYp1w0JsW8f2E0rMU4me_SA7Co14Nm2S8UlmZSemDj0yeKD7P5em5sNOV_0CUKPl2JSHAmZWAqJ1GU_Ni9PMQJSPoMEaGjWmJvwp-0SRuAfja8V4tnB1FFiUapJtdRH_KCWCjo_IXrGJlpg6_7b-zkZ8xl0_GxwOSIk-gOT8150GvMagOBwG8_3W5V3M-GwnpfdQt96kA8WzYNqBVgUUdbe8N821B9ac4_NqG37gna1ubwXtF1MmlQ63XVYkMREGaGWAKY87F0' has set 'SameSite=None' and must also set 'Secure'.

    To Reproduce

    Enable container orchestration and deploy the BFF server to a container registry.

    Create a container app (and supporting resources such as environment, app insights etc.). and deploy the image above.

    On the main index page select the 'Log in' link and you will be redirected to the /MicrosoftIdentity/Account/Error page which states 'Correlation failed.'

    Additional notes

    Deploying to a standard app service plan works fine.

    Deploying to a container based app service plan does not work either, also producing a 'correlation failed' error. This is probably the quickest path to reproducing the issue as it only takes a few minutes to create the app service.

    Many Thanks,

    Roger

  • 2

    Upon app JS Logout(Signout command) does not display Azure AD B2C login page

    Hi Damien,

    VSDotNetGuy again. We see in your template, you're using a form post based logout to the server side api. However, we're using a menu drawer based Logout link that when clicked raises an event. From the event code we post to the server at \api\account\logout using HttpClient.

    The server side Logout method is being called and the Signout command seems to run okay. However, we expected the app would no longer be authenticated so would display the Azure AD B2C login page upon redirect or refresh.

    Does your template prompt for AD login after Signout? If not, any ideas on how to be prompted by Azure AD B2C to login after the server side Signout is completed?

    Thx in advance and take care, Bob Baldwin (aka VSDotNetGuy)

  • 3

    Update readme regarding Azure Container support

    Hello Damien, please find attached the documentation change to describe what to do if experiencing issues after deploying the app to Azure container based services. Apologies if the format is unorthodox or anything, this is my first PR request on someone else's repo! Many Thanks, Roger

  • 4

    Switch to MicrosoftIdentityOptions to support downstream apis

    services.Configure<MicrosoftIdentityOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
    

    instead of

    services.Configure<OpenIdConnectOptions>
    
  • 5

    DirectController

    Hi Damien,

    I've been looking at the AD B2C BFF Blazor template, it looks good, thx for great work.

    I did wonder since include global anti forgery setting in Startup, why did you put [ValidateAntiForgeryToken] on DirectApiController or is this just a left over?

    Thx...Bob Baldwin aka VSDotNetGuy!

  • 6

    Update the GraphApiClaimsTransformation class to use the objectidentifier

    var objectidentifierClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier"; var objectIdentifier = principal.Claims.FirstOrDefault(t => t.Type == objectidentifierClaimType);

  • 7

    Template for Blazor WASM not Hosted

    Hi @damienbod Have you ever done this, or got it on the list of things to try for the future? My app architecture is ASPNET BFF Server, and a standalone Blazor WASM app. I have been trying to hack this template so it isn't intertwined with the WASM Hosted approach but I am struggling with it.

  • 8

    Unhandled exception when logging out after logging-in on multiple browser tabs

    Hi,

    First of all thanks for this fantastic resource!

    I build a Blazor App using the template and can login/logout with my AD B2C tenant. When I open the App in a second tab, I am already signed in (as expected). If I now sign-out in either tab, and then click on the Direct-API link in the navbar on the other tab, I get an exception (in the Blazor Client) that looks like a malformed JSON object is being parsed.

    This happens in Chrome and Safari on MacOS, but (curiously) not on Firefox, there I get redirected to sign in again (as expected).

    I probably did something wrong, but I tried to stay as close to the template as possible. Anyway, I am totally baffled and stuck. Obviously I can give you access to my repository with the Blazor SPA. I can be reached by email on gmail or outlook by using firstname.lastname as name (to get the Client-Secret).

    This is the exception I get:

    crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
          Unhandled exception rendering component: '<' is an invalid start of a value. Path: $ | LineNumber: 1 | BytePositionInLine: 0.
    System.Text.Json.JsonException: '<' is an invalid start of a value. Path: $ | LineNumber: 1 | BytePositionInLine: 0.
     ---> System.Text.Json.JsonReaderException: '<' is an invalid start of a value. LineNumber: 1 | BytePositionInLine: 0.
       at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
       at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker)
       at System.Text.Json.Utf8JsonReader.ReadFirstToken(Byte first)
       at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
       at System.Text.Json.Utf8JsonReader.Read()
       at System.Text.Json.Serialization.JsonConverter`1[[System.String[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
       --- End of inner exception stack trace ---
       at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, JsonReaderException ex)
       at System.Text.Json.Serialization.JsonConverter`1[[System.String[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
       at System.Text.Json.JsonSerializer.ReadCore[String[]](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
       at System.Text.Json.JsonSerializer.ReadCore[String[]](JsonReaderState& readerState, Boolean isFinalBlock, ReadOnlySpan`1 buffer, JsonSerializerOptions options, ReadStack& state, JsonConverter converterBase)
       at System.Text.Json.JsonSerializer.ContinueDeserialize[String[]](ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack, JsonConverter converter, JsonSerializerOptions options)
       at System.Text.Json.JsonSerializer.<ReadAllAsync>d__65`1[[System.String[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
       at System.Net.Http.Json.HttpContentJsonExtensions.<ReadFromJsonAsyncCore>d__4`1[[System.String[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
       at System.Net.Http.Json.HttpClientJsonExtensions.<GetFromJsonAsyncCore>d__13`1[[System.String[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
       at BlazorWasmAadB2cBffTest.Client.Pages.DirectApi.OnInitializedAsync()
       at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
       at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)