{"id":15410556,"url":"https://github.com/damienbod/blazor.bff.azureb2c.template","last_synced_at":"2025-07-02T21:34:00.371Z","repository":{"id":39906856,"uuid":"434539865","full_name":"damienbod/Blazor.BFF.AzureB2C.Template","owner":"damienbod","description":"Blazor.BFF.AzureB2C.Template,  Blazor WASM hosted in ASP.NET Core using Azure B2C BFF (server authentication) and Microsoft Graph","archived":false,"fork":false,"pushed_at":"2024-02-03T16:09:20.000Z","size":4503,"stargazers_count":67,"open_issues_count":4,"forks_count":9,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-05-23T22:08:38.865Z","etag":null,"topics":["aspnetcore","azure","azureb2c","csp","dotnetcore","graph","oauth2"],"latest_commit_sha":null,"homepage":"https://www.nuget.org/packages/Blazor.BFF.AzureB2C.Template","language":"CSS","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/damienbod.png","metadata":{"files":{"readme":"README-NUGET.md","changelog":"Changelog.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2021-12-03T09:33:58.000Z","updated_at":"2025-03-27T16:59:37.000Z","dependencies_parsed_at":"2023-11-12T11:30:55.538Z","dependency_job_id":"228dd6b2-de9d-41e0-85ff-7be9cfbf30c1","html_url":"https://github.com/damienbod/Blazor.BFF.AzureB2C.Template","commit_stats":{"total_commits":114,"total_committers":3,"mean_commits":38.0,"dds":0.03508771929824561,"last_synced_commit":"168cc89beb0961920553016fa5ee5901d4738b0e"},"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"purl":"pkg:github/damienbod/Blazor.BFF.AzureB2C.Template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damienbod%2FBlazor.BFF.AzureB2C.Template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damienbod%2FBlazor.BFF.AzureB2C.Template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damienbod%2FBlazor.BFF.AzureB2C.Template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damienbod%2FBlazor.BFF.AzureB2C.Template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/damienbod","download_url":"https://codeload.github.com/damienbod/Blazor.BFF.AzureB2C.Template/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/damienbod%2FBlazor.BFF.AzureB2C.Template/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263218938,"owners_count":23432582,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aspnetcore","azure","azureb2c","csp","dotnetcore","graph","oauth2"],"created_at":"2024-10-01T16:44:59.804Z","updated_at":"2025-07-02T21:34:00.315Z","avatar_url":"https://github.com/damienbod.png","language":"CSS","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Blazor.BFF.AzureB2C.Template\r\n\r\n[![.NET](https://github.com/damienbod/Blazor.BFF.AzureB2C.Template/actions/workflows/dotnet.yml/badge.svg)](https://github.com/damienbod/Blazor.BFF.AzureB2C.Template/actions/workflows/dotnet.yml) [![NuGet Status](http://img.shields.io/nuget/v/Blazor.BFF.AzureB2C.Template.svg?style=flat-square)](https://www.nuget.org/packages/Blazor.BFF.AzureB2C.Template/) [Change log](https://github.com/damienbod/Blazor.BFF.AzureB2C.Template/blob/main/Changelog.md)\r\n\r\nThis 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 from 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.\r\n\r\n## Features\r\n\r\n- WASM hosted in ASP.NET Core 8\r\n- BFF with Azure B2C using Microsoft.Identity.Web\r\n- OAuth2 and OpenID Connect OIDC\r\n- No tokens in the browser\r\n- Azure AD Continuous Access Evaluation CAE support\r\n\r\n## Using the template\r\n\r\n### install\r\n\r\n```\r\ndotnet new install Blazor.BFF.AzureB2C.Template\r\n```\r\n\r\n### run\r\n\r\n```\r\ndotnet new blazorbffb2c -n YourCompany.Bff\r\n```\r\n\r\nUse 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.\r\n\r\n## Setup after installation\r\n\r\nAdd the Azure B2C App registration settings\r\n\r\n```\r\n\"AzureB2C\": {\r\n\t\"Instance\": \"https://--your-domain--.b2clogin.com\",\r\n\t\"Domain\": \"[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]\",\r\n\t\"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]\",\r\n\t\"ClientId\": \"[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]\",\r\n\t\"ClientSecret\": \"[Copy the client secret added to the app from the Azure portal]\",\r\n\t\"ClientCertificates\": [\r\n\t],\r\n\t// the following is required to handle Continuous Access Evaluation challenges\r\n\t\"ClientCapabilities\": [ \"cp1\" ],\r\n\t\"CallbackPath\": \"/signin-oidc\"\r\n\t// Add your policy here\r\n\t\"SignUpSignInPolicyId\": \"B2C_1_signup_signin\", \r\n\t\"SignedOutCallbackPath \": \"/signout-callback-oidc\"\r\n},\r\n\r\n```\r\n\r\nAdd the permissions for Microsoft Graph if required, application scopes are used due to Azure B2C\r\n\r\n```\r\n\"GraphApi\": {\r\n\t// Add the required Graph permissions to the Azure App registration\r\n\t\"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]\",\r\n\t\"ClientId\": \"[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]\",\r\n\t\"Scopes\": \".default\"\r\n\t//\"ClientSecret\": \"--in-user-secrets--\"\r\n},\r\n```\r\n\r\n### Use Continuous Access Evaluation CAE with a downstream API (access_token)\r\n\r\n#### Azure app registration manifest\r\n\r\n```json\r\n\"optionalClaims\": {\r\n\t\"idToken\": [],\r\n\t\"accessToken\": [\r\n\t\t{\r\n\t\t\t\"name\": \"xms_cc\",\r\n\t\t\t\"source\": null,\r\n\t\t\t\"essential\": false,\r\n\t\t\t\"additionalProperties\": []\r\n\t\t}\r\n\t],\r\n\t\"saml2Token\": []\r\n},\r\n```\r\n\r\nAny API call for the Blazor WASM could be implemented like this:\r\n\r\n```\r\n[HttpGet]\r\npublic async Task\u003cIActionResult\u003e Get()\r\n{\r\n  try\r\n  {\r\n\t// Do logic which calls an API and throws claims challenge \r\n\t// WebApiMsalUiRequiredException. The WWW-Authenticate header is set\r\n\t// using the OpenID Connect standards and Signals spec.\r\n  }\r\n  catch (WebApiMsalUiRequiredException hex)\r\n  {\r\n\tvar claimChallenge = WwwAuthenticateParameters\r\n\t\t.GetClaimChallengeFromResponseHeaders(hex.Headers);\r\n\t\t\r\n\treturn Unauthorized(claimChallenge);\r\n  }\r\n}\r\n```\r\n\r\nThe downstream API call could be implemented something like this:\r\n\r\n```\r\npublic async Task\u003cT\u003e CallApiAsync(string url)\r\n{\r\n\tvar client = _clientFactory.CreateClient();\r\n\r\n\t// ... add bearer token\r\n\t\r\n\tvar response = await client.GetAsync(url);\r\n\tif (response.IsSuccessStatusCode)\r\n\t{\r\n\t\tvar stream = await response.Content.ReadAsStreamAsync();\r\n\t\tvar payload = await JsonSerializer.DeserializeAsync\u003cT\u003e(stream);\r\n\r\n\t\treturn payload;\r\n\t}\r\n\r\n\t// You can check the WWW-Authenticate header first, if it is a CAE challenge\r\n\t\r\n\tthrow new WebApiMsalUiRequiredException($\"Error: {response.StatusCode}.\", response);\r\n}\r\n```\r\n\r\n### Use Continuous Access Evaluation CAE in a standalone app (id_token)\r\n\r\n#### Azure app registration manifest\r\n\r\n```json\r\n\"optionalClaims\": {\r\n\t\"idToken\": [\r\n\t\t{\r\n\t\t\t\"name\": \"xms_cc\",\r\n\t\t\t\"source\": null,\r\n\t\t\t\"essential\": false,\r\n\t\t\t\"additionalProperties\": []\r\n\t\t}\r\n\t],\r\n\t\"accessToken\": [],\r\n\t\"saml2Token\": []\r\n},\r\n```\r\nIf using a CAE Authcontext in a standalone project, you only need to challenge against the claims in the application.\r\n\r\n```\r\nprivate readonly CaeClaimsChallengeService _caeClaimsChallengeService;\r\n\r\npublic AdminApiCallsController(CaeClaimsChallengeService caeClaimsChallengeService)\r\n{\r\n  _caeClaimsChallengeService = caeClaimsChallengeService;\r\n}\r\n\r\n[HttpGet]\r\npublic IActionResult Get()\r\n{\r\n  // if CAE claim missing in id token, the required claims challenge is returned\r\n  var claimsChallenge = _caeClaimsChallengeService\r\n\t.CheckForRequiredAuthContextIdToken(AuthContextId.C1, HttpContext);\r\n\r\n  if (claimsChallenge != null)\r\n  {\r\n\treturn Unauthorized(claimsChallenge);\r\n  }\r\n```\r\n\r\n### uninstall\r\n\r\n```\r\ndotnet new uninstall Blazor.BFF.AzureB2C.Template\r\n```\r\n\r\n### Troubleshooting \r\n \r\nIf running the app in a service such as Web App for Containers or Azure Container apps then you may experience issues with Azure terminating the SSL connection and passing the requests on as HTTP. \r\n \r\nThe first area affected will be the AntiForgery cookie, which will need the SecurePolicy changing as shown below: \r\n \r\n``` \r\nservices.AddAntiforgery(options =\u003e \r\n{ \r\n    options.HeaderName = \"X-XSRF-TOKEN\"; \r\n    options.Cookie.Name = \"__Host-X-XSRF-TOKEN\"; \r\n    options.Cookie.SameSite = SameSiteMode.Strict; \r\n    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; \r\n}); \r\n``` \r\n \r\nThe second area affected will be the login process itself, which will fail with a 'Correlation failed' error. Inspecting the event logs will show errors referring to 'cookie not found'. To remedy this, modify the code in the two areas below: \r\n \r\n``` \r\nbuilder.Services.Configure\u003cForwardedHeadersOptions\u003e(options =\u003e \r\n{ \r\n    options.ForwardedHeaders = ForwardedHeaders.XForwardedProto; \r\n}); \r\n \r\nservices.AddMicrosoftIdentityWebAppAuthentication(configuration, \"AzureB2C\") \r\n    .EnableTokenAcquisitionToCallDownstreamApi(Array.Empty\u003cstring\u003e()) \r\n    .AddInMemoryTokenCaches(); \r\n``` \r\nand this \r\n \r\n``` \r\napp.UseForwardedHeaders(); \r\n \r\nif (env.IsDevelopment()) \r\n{ \r\n    app.UseDeveloperExceptionPage(); \r\n``` \r\n \r\nFurther details may be found here [Configure ASP.NET Core to work with proxy servers and load balancers](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-3.1#nginx-configuration) \r\n \r\nPlease note, adding the 'XForwardedFor' enum as shown in the Microsoft document above did not work and needed to be removed so only the XForwardedProto remains. \r\n\r\n## Credits, Used NuGet packages + ASP.NET Core 8.0 standard packages\r\n\r\n- NetEscapades.AspNetCore.SecurityHeaders\r\n\r\n## Links\r\n\r\nhttps://github.com/AzureAD/microsoft-identity-web\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdamienbod%2Fblazor.bff.azureb2c.template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdamienbod%2Fblazor.bff.azureb2c.template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdamienbod%2Fblazor.bff.azureb2c.template/lists"}