The article shows how an Angular nx Standalone UI hosted in an ASP.NET Core application can be secured using cookies. Auth0 is used as the identity provider. The trusted application is protected using the Open ID Connect code flow with a secret and using PKCE. The API calls are protected using the secure cookie and anti-forgery tokens to protect against CSRF. This architecture is also known as the Backend for Frontend (BFF) Pattern.
Code: https://github.com/damienbod/bff-auth0-aspnetcore-angular
Auth0 Setup
An Auth0 account is required and a Regular Web Application was setup for this. This is not an SPA application and must always be deployed with a backend which can keep a secret. The Angular client can only use the APIs on the same domain and uses cookies. All application authentication is implemented in the trusted backend and the secure data is encrypted in the cookie.
Architecture Setup
The application is setup to authenticate as one and remove the sensitive data from the client browser. The single security context has UI logic implemented in Angular and server logic, including the security flows, implemented in ASP.NET Core. The server part of the application handles all request from the client application and the client application should only use the APIs from the same ASP.NET Core implemented host. Cookies are used to send the secure API requests. The UI implementation is greatly simplified and the backend application can add additional security features as it is a confidential client, or trusted client.
ASP.NET Core Setup
The ASP.NET Core application is setup to authenticate using OpenID Connect and to store this session in a secure cookie. All OpenID Connect providers require small specific flavors of OpenID Connect. The different OpenID Connect clients can all be implemented using the standard ASP.NET Core AddOpenIdConnect method. If you want, most identity providers provide product specific clients which just wrap this client and change the names of the methods, and pre-configure the provider server specifics. When using the client specific clients, you need to re-learn the APIs for the different OpenID Connect servers. The following code implements the OpenID Connect client for Auth0 and also acquires a delegated access token for the required scope. This is not required, just added as documentation.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.Name = "__Host-auth0";
options.Cookie.SameSite = SameSiteMode.Lax;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = $"https://{configuration["Auth0:Domain"]}";
options.ClientId = configuration["Auth0:ClientId"];
options.ClientSecret = configuration["Auth0:ClientSecret"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("auth0-user-api-one");
// options.CallbackPath = new PathString("/signin-oidc");
options.ClaimsIssuer = "Auth0";
options.SaveTokens = true;
options.UsePkce = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters.NameClaimType = "name";
options.Events = new OpenIdConnectEvents
{
// handle the logout redirection
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var logoutUri = $"https://{configuration["Auth0:Domain"]}/v2/logout?client_id={configuration["Auth0:ClientId"]}";
var postLogoutUri = context.Properties.RedirectUri;
if (!string.IsNullOrEmpty(postLogoutUri))
{
if (postLogoutUri.StartsWith("/"))
{
// transform to absolute
var request = context.Request;
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
}
logoutUri += $"&returnTo={Uri.EscapeDataString(postLogoutUri)}";
}
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
},
OnRedirectToIdentityProvider = context =>
{
// The context's ProtocolMessage can be used to pass along additional query parameters
// to Auth0's /authorize endpoint.
//
// Set the audience query parameter to the API identifier to ensure the returned Access Tokens can be used
// to call protected endpoints on the corresponding API.
context.ProtocolMessage.SetParameter("audience", "https://auth0-api1");
return Task.FromResult(0);
}
};
});
The OpenID Connect client for Auth0 using the configuration from the appsettings or
"Auth0": {
"Domain": "your-domain-in-auth0",
"ClientId": "--in-secrets--",
"ClientSecret": "--in-secrets--"
}
The API controller uses the secure cookie and the CSRF protection.
[ValidateAntiForgeryToken]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
[ApiController]
[Route("api/[controller]")]
public class DirectApiController : ControllerBase
{
[HttpGet]
public async Task<IEnumerable<string>> GetAsync()
{
// if you need a delegated access token for downstream APIs
var accessToken = await HttpContext.GetTokenAsync("access_token");
return new List<string> { "some data", "more data", "loads of data" };
}
}
Angular Setup
The Angular solution for development and production is setup like described in this blog:
The UI part of the application implements no OpenID connect flows and is always part of the server application. The UI can only access APIs from the single hosting application.
Links
https://github.com/damienbod/bff-aspnetcore-angular
https://learn.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core
https://nx.dev/getting-started/intro
https://github.com/isolutionsag/aspnet-react-bff-proxy-example
[…] Secure Angular application using Auth0 and ASP.NET Core with BFF (Damien Bowden) […]
[…] Secure Angular application using Auth0 and ASP.NET Core with BFF – Damien Bowden […]
[…] sample written by Doğan Erişen earlier this year. [On September 18th, 2023, Damien Bowd published this post describing the use of the BFF architecture to secure an Angular SPA with Auth0 using ASP.NET […]