Building and configuring backend for frontend (BFF):¶
Backend for frontend pattern is a recommended way how to build your custom application and leverage the DHI MIKE Cloud Platform.
A reference implementation of this pattern is in DHI's internal https://github.com/DHI/Demo-SPA-SSO-MikeCloud repository.
The BFF server hides the OAuth2 access tokens from from the frontend so they are as safe as they can be. The authentication between the frontend and BFF is cookie based. The BFF may serve as a simple proxy server to the Platform endpoints, but it can also modify, collect, cache, or combine data from multiple Platform or third party services. The BFF can also have its own database and other resources to support any kind of value added functionality.
The Program.cs
of any application can contain all kinds of service definitions and configuration, so it is not practical to recommend how exactly should your Program.cs
look like. Instead we provide extension methods that should help you configure cookie based authentication between your frontend and backend and also add Platform Clients so you can interact with the Platform using the Platform SDK.
The end user will experience the standard DHI login experience including tenant selection. Note that the tenant selector can be avoided if you specify tenant during the login process (e.g. https://login.mike-cloud.com/authorize?clientId=<my client id>&tenantId=<my tenant id>&...
).
Before you start building an application, you need to register a client Id, client secret, and response URL(s) with the Platform. In addition to that you should define which platform scopes will be used by your application and optionally also define any custom scopes for your application.
Session vs Cache:¶
There are two out-of-the-box options for configuring the Platform authentication and services in your application: session or cache. Either option is fine for the Platform, but you should choose the right option for your application. You can also implement a custom option too.
In any case, you application settings should include something like:
"Auth": {
"Authority": "https://login.mike-cloud-dev.com",
"ClientId": "your client id",
"ClientSecret": "your client secret",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath": "/signout-callback-oidc",
"SignedOutRedirectUri": "https://localhost:7080/",
"Scope": ["platform_<scope>", "<custom_scope>" , ...],
}
CacheSlidingExpirationMinutes
option.
Session¶
The PlatformWebSessionAccessTokenProvider
remembers tokens by user session, the user will have separate set of these tokens when they log in from a different browser or a different device. Note that you need to specify corresponding ConfigureOidcOptionsWithSession
when adding Open ID configuration. If you use this option, your Program.cs
must include:
// ...
services.AddDistributedMemoryCache(); // needed also for the session
services.AddPlatformSession();
// ...
services.AddPlatformAuthentication()
.AddOpenId<ConfigureOidcOptionsWithSession>(builder.Configuration);
// ...
services.AddAuthorization(opt =>
{
opt.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
});
// ...
services.AddAuthorization(opt => opt.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
// ...
services.AddPlatformClientsWithTokenProvider<PlatformWebSessionAccessTokenProvider>(o => o.ConfigureAccessToken());
// ... after the builder builds the WebApplication as app:
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
Cache¶
PlatformTokenDistributedCacheAccessTokenProvider
remembers tokens by user principal name. That means, the user will use the same set of authentication tokens from any browser or device. The token data are stored in a distributed cache. Note that you need to specify corresponding ConfigureOidcOptionsWithDistributedCache
when adding Open ID configuration. If you use this option, your Program.cs
must include:
// ...
services.AddDistributedMemoryCache(); // fine for development, but add e.g. Redis for production
// ...
services.AddPlatformAuthentication(builder.Environment.EnvironmentName)
.AddOpenId<ConfigureOidcOptionsWithDistributedCache>(builder.Configuration);
// ...
services.AddAuthorization(opt => opt.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
// ...
services.AddPlatformClientsWithTokenProvider<PlatformWebDistributedCacheAccessTokenProvider>(o => o.ConfigureAccessToken());
// ... after the builder builds the WebApplication as app:
app.UseAuthentication();
app.UseAuthorization();
Distributed memory cache and scalability¶
When an application can scale to multiple instances, any instance must be able to access the same session data. The reference implementation uses a basic distributed memory cache implementation from Microsoft (services.AddDistributedMemoryCache()
). However, this implementation is in fact not distributed, it only works for single instance applications. Production applications that could scale into multiple instances must use an actual distributed cache, such as Redis cache or Community.Microsoft.Extensions.Caching.PostgreSQL.
If you use Redis, database, or other cache, and you want the tokens to be encrypted, consider adding ASP.NET DataProtection to protect the tokens. See also Configure ASP.NET Core Data Protection for further details.
Authentication and Session expiration¶
Whether you use a session or cache Platform Token Provider on the backend, the session (or cache entry) and the user sign in will expire after a defined interval of inactivity (i.e. no requests with the particular cookie). By default, we set these limits to 30 minutes. You can override them using parameters of the configuration methods. We strongly recommend to set the expiration times of the authentication cookie, session, and/or cache entries to the same value.
In our PlatformWeb*TokenProvider
implementations we throw an exception when the user is not authenticated. This setup is sufficient for many applications. However, if your application handles any sensitive data, the frontend should actively call the sing out (log out) endpoint for the user after a set interval of inactivity and also clear cookies used by the application in the browser.
Scopes and Platform Scopes¶
The reference implementation app uses the default scopes. When you build the application, set the necessary scopes in OpenIdConnectOptions*.Scope
property.
CORS¶
If you host frontend as a Single Page Application (SPA) directly from your backend app, you don't need to configure any extra CORS options as the frontend runs on the same host as the backend. However, deploying frontend as a separate application may be useful if:
- you want more control over when a frontend and backend get deployed.
- you want to host the frontend as SPA from CDN or some kind of blob storage.
If you deploy frontend separately, i.e. to a different host, you will need to Add CORS to the service collection:
services.AddCors(o => o.AddPolicy("AuthCorsPolicy", corsPolicyBuilder => {
corsPolicyBuilder
.WithOrigins("https://my-frotnend-host.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
;
}));
AllowAnyOrigin()
is not possible, because AllowCredentials()
is required by the cookie authorization and session mechanism. It is not allowed to combine it with AllowAnyOrigin()
.
After you build the app, use CORS:
app.UseCors();
Configure headers¶
Use app.UseSecurityHeaders()
to configure the default set of security headers. Your application may have special header policies that you can configure by passing a header policy collection to this method. See the Sample app for recommended header configuration.
GDPR and cookies¶
General Data Protection Regulation (GDPR) in Europe and other data protection acts around the world require applications to inform the user what cookies and for what purpose are used. Since both authentication cookie and session cookie are essential for the function of the application the user explicitly requested to use, it is enough to inform the user that these cookies are used. This can be done in a cookie policy presented to the user. However, an application that uses other than the strictly necessary cookies (e.g. marketing, advertizing, sharing data with partners, etc.) is required to inform the user about these cookies and their purpose. It also needs the user's consent to put each cookie in the client storage (e.g. browser storage). See for example https://www.dhigroup.com/ and DHI Privacy Policy.
XSRF/CSRF¶
If your application backend is intended purely for serving your frontend, use antiforgery tokens to prevent XSRF/CSRF attacks. See Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core.
For a React frontend app with C# backend, this means: 1. Create backend endpoint to get antiforgery token 2. Configure backend to validate antiforgery tokens. Note that when using ASP.NET core with cookie based authentication, it is necessary to add mvc controllers including views even for API applications, like this:
services.AddAntiforgery(o => {
o.HeaderName = "X-XSRF-TOKEN";
o.Cookie.Name = "XSRF-TOKEN";
});
// ...
services.AddControllersWithViews(o =>
{
o.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
// ...
app.MapAntiforgeryEndpoint("antiforgery/token", "XSRF-TOKEN");
- Antiforgery should be useable without views #22189
- ASP.NET Core Anti-Forgery Explained for further discussion about configuring XSRF/CSRF in ASP.NET core.
-
How to implement csrf protection for cross domain requests for handling XSRF token when frontend is hosted on a separate domain.
-
In frontend, get an antiforgery token from the endpoint
- You need only one antiforgery token for the application instance so you can store in a global scope and reuse it with every request.
- Send the antiforgery token in request header with every other request to your backend. E.g.:
response = await = fetch("https://my-app-bff.mike-cloud.com/api/project/list", { headers: { "Content-Type": "application/json", // if working with JSON API "X-XSRF-TOKEN": xsrfToken // set the antiforgery header }, credentials: "same-origin" // To include session, authorization, and other cookies with the request. });
Cookie authentication with SignalR and gRPC¶
The Platform publishes events that can be consumed over SignalR. When using cookie based authentication on frontend, the authentication cookie will not be compatible with SignalR connection between the backend and the Platform. You may need to proxy the SignalR events using the backend. For further information see Authentication and authorization in ASP.NET Core SignalR. gRPC calls between the cookie based authenticated frontend and the Platform will not work out of the box either, you will need to proxy them through the backend too. Please see gRPC Authentication.