This article shows how an ASP.NET Core MVC application using Angular in the razor views can be secured using IdentityServer4 and the OpenID Connect Hybrid Flow. The user interface uses server side rendering for the MVC views and the Angular app is then implemented in the razor view. The required security features can be added to the application easily using ASP.NET Core, which makes it safe to use the OpenID Connect Hybrid flow, which once authenticated and authorised, saves the token in a secure cookie. This is not an SPA application, it is an ASP.NET Core MVC application with Angular in the razor view. If you are implementing an SPA application, you should use the OpenID Connect Implicit Flow.
Code: https://github.com/damienbod/AspNetCoreMvcAngular
IdentityServer4 configuration for OpenID Connect Hybrid Flow
IdentityServer4 is implemented using ASP.NET Core Identity with SQLite. The application implements the OpenID Connect Hybrid flow. The client is configured to allow the required scopes, for example the ‘openid’ scope must be added and also the RedirectUris property which implements the URL which is implemented on the client using the ASP.NET Core OpenID middleware.
using IdentityServer4; using IdentityServer4.Models; using System.Collections.Generic; namespace QuickstartIdentityServer { public class Config { public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email(), new IdentityResource("thingsscope",new []{ "role", "admin", "user", "thingsapi" } ) }; } public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("thingsscope") { ApiSecrets = { new Secret("thingsscopeSecret".Sha256()) }, Scopes = { new Scope { Name = "thingsscope", DisplayName = "Scope for the thingsscope ApiResource" } }, UserClaims = { "role", "admin", "user", "thingsapi" } } }; } // clients want to access resources (aka scopes) public static IEnumerable<Client> GetClients() { // client credentials client return new List<Client> { new Client { ClientName = "angularmvcmixedclient", ClientId = "angularmvcmixedclient", ClientSecrets = {new Secret("thingsscopeSecret".Sha256()) }, AllowedGrantTypes = GrantTypes.Hybrid, AllowOfflineAccess = true, RedirectUris = { "https://localhost:44341/signin-oidc" }, PostLogoutRedirectUris = { "https://localhost:44341/signout-callback-oidc" }, AllowedCorsOrigins = new List<string> { "https://localhost:44341/" }, AllowedScopes = new List<string> { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.OfflineAccess, "thingsscope", "role" } } }; } } }
MVC Angular Client Configuration
The ASP.NET Core MVC application with Angular is implemented as shown in this post: Using Angular in an ASP.NET Core View with Webpack
The cookie authentication middleware is used to store the access token in a cookie, once authorised and authenticated. The OpenIdConnectAuthentication middleware is used to redirect the user to the STS server, if the user is not authenticated. The SaveTokens property is set, so that the token is persisted in the secure cookie.
app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationScheme = "Cookies" }); app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions { AuthenticationScheme = "oidc", SignInScheme = "Cookies", Authority = "https://localhost:44348", RequireHttpsMetadata = true, ClientId = "angularmvcmixedclient", ClientSecret = "thingsscopeSecret", ResponseType = "code id_token", Scope = { "openid", "profile", "thingsscope" }, GetClaimsFromUserInfoEndpoint = true, SaveTokens = true });
The Authorize attribute is used to secure the MVC controller or API.
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; namespace AspNetCoreMvcAngular.Controllers { [Authorize] public class HomeController : Microsoft.AspNetCore.Mvc.Controller { public IActionResult Index() { return View(); } public IActionResult Error() { return View(); } } }
CSP: Content Security Policy in the HTTP Headers
Content Security Policy helps you reduce XSS risks. The really brilliant NWebSec middleware can be used to implement this as required. Thanks to André N. Klingsheim for this excellent library. The middleware adds the headers to the HTTP responses.
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
In this configuration, mixed content is not allowed and the scripts can only be used from a local source. Unsafe inline styles are allowed.
app.UseCsp(opts => opts .BlockAllMixedContent() .ScriptSources(s => s.Self()) .StyleSources(s => s.UnsafeInline()) );
Set the Referrer-Policy in the HTTP Header
This allows us to restrict the amount of information being passed on to other sites when referring to other sites.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
Scott Helme write a really good post on this:
https://scotthelme.co.uk/a-new-security-header-referrer-policy/
Again NWebSec middleware is used to implement this.
app.UseReferrerPolicy(opts => opts.NoReferrer());
Secure Cookies
Only secure cookies should be used to store the session information. The anti-forgery cookie is an exception to this.
You can check this in the Chrome browser:
Image may be NSFW.
Clik here to view.
XFO: X-Frame-Options
The X-Frame-Options Headers can be used to prevent an IFrame from being used from within the UI. This helps protect against click jacking.
https://developer.mozilla.org/de/docs/Web/HTTP/Headers/X-Frame-Options
app.UseXfo(xfo => xfo.Deny());
Configuring HSTS: Http Strict Transport Security
The HTTP Header tells the browser to force HTTPS for a length of time.
app.UseHsts(hsts => hsts.MaxAge(365).IncludeSubdomains());
TOFU (Trust on first use) or first time loading.
Once you have a proper cert and a fixed URL, you can configure that the browser to preload HSTS settings for your website.
https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet
X-Xss-Protection NWebSec
Adds a middleware to the ASP.NET Core pipeline that sets the X-Xss-Protection (Docs from NWebSec)
app.UseXXssProtection(options => options.EnabledWithBlockMode());
CORS
Only the allowed CORS should be enabled when implementing this. Disabled this as much as possible.
Validating the security Headers
Once you start the application, you can check that all the security headers are added as required:
Image may be NSFW.
Clik here to view.
Here’s the Configure method with all the NWebsec app settings as well as the authentication middleware for the client MVC application.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); loggerFactory.AddSerilog(); //Registered before static files to always set header app.UseHsts(hsts => hsts.MaxAge(365).IncludeSubdomains()); app.UseXContentTypeOptions(); app.UseReferrerPolicy(opts => opts.NoReferrer()); app.UseCsp(opts => opts .BlockAllMixedContent() .ScriptSources(s => s.Self()) .StyleSources(s => s.UnsafeInline()) ); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationScheme = "Cookies" }); app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions { AuthenticationScheme = "oidc", SignInScheme = "Cookies", Authority = "https://localhost:44348", RequireHttpsMetadata = true, ClientId = "angularmvcmixedclient", ClientSecret = "thingsscopeSecret", ResponseType = "code id_token", Scope = { "openid", "profile", "thingsscope" }, GetClaimsFromUserInfoEndpoint = true, SaveTokens = true }); var angularRoutes = new[] { "/default", "/about" }; app.Use(async (context, next) => { if (context.Request.Path.HasValue && null != angularRoutes.FirstOrDefault( (ar) => context.Request.Path.Value.StartsWith(ar, StringComparison.OrdinalIgnoreCase))) { context.Request.Path = new PathString("/"); } await next(); }); app.UseDefaultFiles(); app.UseStaticFiles(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); //Registered after static files, to set headers for dynamic content. app.UseXfo(xfo => xfo.Deny()); app.UseRedirectValidation(); //Register this earlier if there's middleware that might redirect. app.UseXXssProtection(options => options.EnabledWithBlockMode()); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
Links:
https://www.scottbrady91.com/OpenID-Connect/OpenID-Connect-Flows
https://docs.nwebsec.com/en/latest/index.html
https://github.com/NWebsec/NWebsec
https://content-security-policy.com/
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
https://scotthelme.co.uk/a-new-security-header-referrer-policy/
https://developer.mozilla.org/de/docs/Web/HTTP/Headers/X-Frame-Options
https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet
https://gun.io/blog/tofu-web-security/
https://en.wikipedia.org/wiki/Trust_on_first_use
http://www.dotnetnoob.com/2013/07/ramping-up-aspnet-session-security.html
http://openid.net/specs/openid-connect-core-1_0.html
Image may be NSFW.
Clik here to view.
Clik here to view.
