This article shows how to implement authentication and secure a Blazor WASM application hosted in ASP.NET Core using the backend for frontend (BFF) security architecture to authenticate. All security is implemented in the backend and the Blazor WASM is a view of the ASP.NET Core application, no security is implemented in the public client. The application is a trusted client and a secret is used to authenticate the application as well as the identity. The Blazor WASM UI can only use the hosted APIs on the same domain.
Code https://github.com/damienbod/AspNetCoreOpeniddict
Setup
The Blazor WASM and the ASP.NET Core host application is implemented as a single application and deployed as one. The server part implements the authentication using OpenID Connect. OpenIddict is used to implement the OpenID Connect server application. The code flow with PKCE and a user secret is used for authentication.
Open ID Connect Server setup
The OpenID Connect server is implemented using OpenIddict. The is standard implementation as like the documentation. The worker class implements the IHostService interface and is used to add the code flow client used by the Blazor ASP.NET Core application. PKCE is added as well as a client secret.
{
var manager = provider.GetRequiredService<IOpenIddictApplicationManager>();
// Blazor Hosted
if (await manager.FindByClientIdAsync(“blazorcodeflowpkceclient”) is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = “blazorcodeflowpkceclient”,
ConsentType = ConsentTypes.Explicit,
DisplayName = “Blazor code PKCE”,
DisplayNames =
{
[CultureInfo.GetCultureInfo(“fr-FR”)] = “Application cliente MVC”
},
PostLogoutRedirectUris =
{
new Uri(“https://localhost:44348/signout-callback-oidc”),
new Uri(“https://localhost:5001/signout-callback-oidc”)
},
RedirectUris =
{
new Uri(“https://localhost:44348/signin-oidc”),
new Uri(“https://localhost:5001/signin-oidc”)
},
ClientSecret = “codeflow_pkce_client_secret”,
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Permissions.Endpoints.Token,
Permissions.Endpoints.Revocation,
Permissions.GrantTypes.AuthorizationCode,
Permissions.GrantTypes.RefreshToken,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Email,
Permissions.Scopes.Profile,
Permissions.Scopes.Roles,
Permissions.Prefixes.Scope + “dataEventRecords”
},
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
}
Blazor client Application
The client application was created using the Blazor.BFF.OpenIDConnect.Template Nuget template package. The configuration is read from the app settings using the OpenIDConnectSettings section. You could add more configurations if required. This is otherwise a standard OpenID Connect client and will work with any OIDC compatible server. PKCE is required and also a secret to validate the application. The AddAntiforgery method is used so that API calls can be forced to validate anti-forgery token to protect against CSRF as well as the same site cookie protection.
{
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.AddHttpClient();
services.AddOptions();
;
var openIDConnectSettings = Configuration.GetSection(“OpenIDConnectSettings”);
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.SignInScheme = “Cookies”;
options.Authority = openIDConnectSettings[“Authority”];
options.ClientId = openIDConnectSettings[“ClientId”];
options.ClientSecret = openIDConnectSettings[“ClientSecret”];
options.RequireHttpsMetadata = true;
options.ResponseType = “code”;
options.UsePkce = true;
options.Scope.Add(“profile”);
options.Scope.Add(“offline_access”);
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
//options.ClaimActions.MapUniqueJsonKey(“preferred_username”, “preferred_username”);
});
services.AddControllersWithViews(options =>
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()));
services.AddRazorPages().AddMvcOptions(options =>
{
//var policy = new AuthorizationPolicyBuilder()
// .RequireAuthenticatedUser()
// .Build();
//options.Filters.Add(new AuthorizeFilter(policy));
});
}
The OIDC configuration settings are read from the OpenIDConnectSettings section. This can be extended if further specific settings are required.
“Authority”: “https://localhost:44395”,
“ClientId”: “blazorcodeflowpkceclient”,
“ClientSecret”: “codeflow_pkce_client_secret”
},
The NetEscapades.AspNetCore.SecurityHeaders Nuget package is used to add security headers to the application to protect the session. The configuration is setup for Blazor.
{
var policy = new HeaderPolicyCollection()
.AddFrameOptionsDeny()
.AddXssProtectionBlock()
.AddContentTypeOptionsNoSniff()
.AddReferrerPolicyStrictOriginWhenCrossOrigin()
.AddCrossOriginOpenerPolicy(builder =>
{
builder.SameOrigin();
})
.AddCrossOriginResourcePolicy(builder =>
{
builder.SameOrigin();
})
.AddCrossOriginEmbedderPolicy(builder => // remove for dev if using hot reload
{
builder.RequireCorp();
})
.AddContentSecurityPolicy(builder =>
{
builder.AddObjectSrc().None();
builder.AddBlockAllMixedContent();
builder.AddImgSrc().Self().From(“data:”);
builder.AddFormAction().Self().From(idpHost);
builder.AddFontSrc().Self();
builder.AddStyleSrc().Self();
builder.AddBaseUri().Self();
builder.AddFrameAncestors().None();
// due to Blazor
builder.AddScriptSrc()
.Self()
.WithHash256(“v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA=”)
.UnsafeEval();
// due to Blazor hot reload requires you to disable script and style CSP protection
// if using hot reload, DO NOT deploy an with an insecure CSP
})
.RemoveServerHeader()
.AddPermissionsPolicy(builder =>
{
builder.AddAccelerometer().None();
builder.AddAutoplay().None();
builder.AddCamera().None();
builder.AddEncryptedMedia().None();
builder.AddFullscreen().All();
builder.AddGeolocation().None();
builder.AddGyroscope().None();
builder.AddMagnetometer().None();
builder.AddMicrophone().None();
builder.AddMidi().None();
builder.AddPayment().None();
builder.AddPictureInPicture().None();
builder.AddSyncXHR().None();
builder.AddUsb().None();
});
if (!isDev)
{
// maxage = one year in seconds
policy.AddStrictTransportSecurityMaxAgeIncludeSubDomains(maxAgeInSeconds: 60 * 60 * 24 * 365);
}
return policy;
}
The APIs used by the Blazor UI are protected by the ValidateAntiForgeryToken and the Authorize attribute. You could add authorization as well if required. Cookies are used for this API with same site protection.
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[ApiController]
[Route(“api/[controller]”)]
public class DirectApiController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
return new List<string> { “some data”, “more data”, “loads of data” };
}
}
When the application is started, the user can sign-in and authenticate using OpenIddict.
The setup keeps all the security implementation in the trusted backend. This setup can work against any OpenID Connect conform server. By having a trusted application, it is now possible to implement access to downstream APIs in a number of ways and possible to add further protections as required. The downstream API does not need to be public either. You should only use a downstream API if required. If a software architecture forces you to use APIs from separate domains, then a YARP reverse proxy can be used to access to API, or a service to service API call, ie trusted client with a trusted server, or an on behalf flow (OBO) flow can be used.
Links
https://documentation.openiddict.com/
https://github.com/damienbod/Blazor.BFF.OpenIDConnect.Template
https://github.com/andrewlock/NetEscapades.AspNetCore.SecurityHeaders