Secure ASP.NET Core MVC with Angular using IdentityServer4 OpenID Connect Hybrid Flow

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

Blogs in this Series

History

2020-06-27 Updated to ASP.NET Core 3.1, IdentityServer4 4.0.0 Angular 10.0.0
2019-08-27 Updated to ASP.NET Core 3.0, Angular 8.2.3
2018-06-16 Updated to ASP.NET Core 2.1, Angular 6.0.5
2017-11-05 Updated to Angular 5 and Typescript 2.6.1
2017-09-22 Updated to ASP.NET Core 2.0, Angular 4.4.3

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.

// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using IdentityServer4;
using IdentityServer4.Models;
using System.Collections.Generic;

namespace StsServerIdentity
{
    public class Config
    {
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email()
            };
        }

        public static IEnumerable<ApiScope> GetApiScopes()
        {
            return new List<ApiScope>
            {
                new ApiScope("thingsscope", "Scope for the thingsscope"),
                //new ApiScope("securedFiles",  "Scope for the securedFiles ApiResource")
            };
        }

        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("thingsscopeApi")
                {
                    ApiSecrets =
                    {
                        new Secret("thingsscopeSecret".Sha256())
                    },
                    UserClaims = { "role", "admin", "user", "thingsapi" },
                    Scopes = { "thingsscope" }
                }
            };
        }

        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "angularmvcmixedclient",
                    ClientId = "angularmvcmixedclient",
                    ClientSecrets = {new Secret("thingsscopeSecret".Sha256()) },
                    AllowedGrantTypes = GrantTypes.Hybrid,
                    AllowOfflineAccess = true,
                    RequireConsent = true,
                    RequirePkce = false,
                    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.

services.AddAuthentication(options => {
	options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
	options.SignInScheme = "Cookies";
	options.Authority = "https://localhost:44348";
	options.RequireHttpsMetadata = true;
	options.ClientId = "angularmvcmixedclient";
	options.ClientSecret = "thingsscopeSecret";
	options.ResponseType = "code id_token";
	options.Scope.Add("thingsscope");
	options.Scope.Add("profile");
	//options.Scope.Add("offline_access");
	options.UsePkce = false;
	// NOT WORKING in ID4 version 4.0.0 => bug
	//options.Prompt = "login"; // select_account login consent
	options.SaveTokens = true;
});


services.AddAuthorization();

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 unsafe inline styles are allowed.

app.UseCsp(opts => opts
	.BlockAllMixedContent()
	.ScriptSources(s => s.Self()).ScriptSources(s => s.UnsafeEval())
	.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());

Redirect Validation

You can secure that application so that only redirects to your sites are allowed. For example, only a redirect to IdentityServer4 is allowed.

// Register this earlier if there's middleware that might redirect.
// The IdentityServer4 port needs to be added here. 
// If the IdentityServer4 runs on a different server, this configuration needs to be changed.
app.UseRedirectValidation(t => t.AllowSameHostRedirectsToHttps(44348)); 

Secure Cookies

Only secure cookies should be used to store the session information.

You can check this in the Chrome browser:

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://hstspreload.org/

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.

Cross Site Request Forgery XSRF

See this blog:
Anti-Forgery Validation with ASP.NET Core MVC and Angular

Validating the security Headers

Once you start the application, you can check that all the security headers are added as required:

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, IWebHostEnvironment env, IAntiforgery antiforgery)
{
	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	else
	{
		app.UseExceptionHandler("/Home/Error");
		// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
		app.UseHsts();
	}

	//Registered before static files to always set header
	app.UseXContentTypeOptions();
	app.UseReferrerPolicy(opts => opts.NoReferrer());
	app.UseCsp(opts => opts
		.BlockAllMixedContent()
		.ScriptSources(s => s.Self()).ScriptSources(s => s.UnsafeEval())
		.StyleSources(s => s.UnsafeInline())
	);

	JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

	var angularRoutes = new[] {
		 "/default",
		 "/about"
	 };

	app.UseDefaultFiles();
	app.UseStaticFiles();

	//Registered after static files, to set headers for dynamic content.
	app.UseXfo(xfo => xfo.Deny());
	app.UseRedirectValidation(t => t.AllowSameHostRedirectsToHttps(44348)); 
	app.UseXXssProtection(options => options.EnabledWithBlockMode());

	app.UseRouting();
	app.UseAuthentication();
	app.UseAuthorization();

	app.Use(async (context, next) =>
	{
		string path = context.Request.Path.Value;
		if (path != null && !path.ToLower().Contains("/api"))
		{
			// XSRF-TOKEN used by angular in the $http if provided
			var tokens = antiforgery.GetAndStoreTokens(context);
			context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
				new CookieOptions() { HttpOnly = false });
		}

		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.UseEndpoints(endpoints =>
	{
		endpoints.MapControllerRoute(
			name: "default",
			pattern: "{controller=Home}/{action=Index}/{id?}");
	});
}

Links:

https://www.scottbrady91.com/OpenID-Connect/OpenID-Connect-Flows

https://docs.nwebsec.com/en/latest/index.html

https://www.nwebsec.com/

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

https://www.ssllabs.com/

20 comments

  1. […] Secure ASP.NET Core MVC with Angular using IdentityServer4 OpenID Connect Hybrid Flow (Damien Bowden) […]

  2. Nice example, how do you handle token expiration?

    1. I validation this in the client app and also the server API. I intend to implement the session management spec from OpenID Connect which will refresh the token .

      Greetings Damien

  3. Rusty · · Reply

    Hi Damien, I’ve enjoyed reading and learning from your blogs. I have a question on this one. After pulling down a zip of the code, I open in VS 2017 (fully updated at the time to 15.2). After letting the solution pull down dependencies for both projects and letting the task runner kick in, I build with no issues.

    Upon running (F5), I receive an error from Visual Studio stating that ‘An error occurred attempting to determine the process id of dotnet.exe which is hosting your application. One or more errors occurred.’.

    Reading other areas, this seemed to be the result of SSL. I ran a CTRL-F5 as recommended to install the certificate. The browser was opened to https://localhost:44341 but with an unhandled exception error. WinHttpException: A connection with the server could not be established. Unable to retrieve document from ‘https://localhost:44348’.

    I know my issue is more than likely a simple configuration in getting SSL set up but I have not been successful just yet. Any suggestions or links on setting up the identity server project to run properly on SSL on a local workstation?

    Thanks.

  4. Rusty · · Reply

    Please disregard / delete my last post. As soon as I submitted, I figured it out (needed to get cert installed). Thanks again and love your posts!

  5. Tomas · · Reply

    Hi damien,

    I want to create an angular SPA with a native mobile app in the feature. Why you says that i cant use hybrid flow with SPA.

  6. Hi Tomas, the OpenID Implicit flow would be more suited for SPAs. Cookies with client applications are open to CSRF attacks

    1. Tomas · · Reply

      Ok thanks, but mobile app can use implicit flow? how they refresh the token?

  7. nick khan · · Reply

    hi damienbod,
    great job on writing up that tutorial on using angular inside aspnet core and also securing the application with hybrid flow.
    i had a debate with another mvp for https://gitter.im/openiddict/openiddict-core
    and they completely disagree with this approach for angular clients.
    their argument is for angular apps, hybrid flow should not be used and implicit should be used.

    which app is getting authenticated here the mvc or the angular? are you available on https://gitter.im/ ?

    1. Hi Nick, thanks for your comment.Using an Angular SPA, the Implicit Flow is the correct approach, and recommended solution. This application is a server side rendered MVC client application which uses Angular in the razor views. The MVC client does the oidc flow, and the requests are protected with Anti forgery tokens, so I see this approach as safe.

      Greetings Damien

      1. Hi Damien,

        Just curious – what would you do for an Angular SPA that hits a .NET Core Web API to retrieve data? Curious as to which flow you would choose and how tokens would be handled? Any help would be greatly appreciated.

      2. Hi Damien,

        Curious as to what should be used for an Angular SPA hitting a .NET Core Web API to retrieve data? Specifically, what flow should be used and how should refresh tokens be handled? Any help would be greatly appreciated.

        Thanks
        – Pat

      3. pjohnsonetg · ·

        Hi Damien,

        Could you please comment on the correct architecture for using an Angular SPA with a .NET Core Web API to retrieve data? Specifically curious what flow you would suggest and how you’d handle refresh tokens?

        Thanks,
        Pat

  8. Hi Damien,

    Very nice example. I have downloaded your sample project and trying to add logout functionality.

    I have implemented new Account controller with LogOut method that I call from angular.

    public async Task Logout()
    {
    await HttpContext.SignOutAsync(
    CookieAuthenticationDefaults.AuthenticationScheme);
    await HttpContext.SignOutAsync(
    OpenIdConnectDefaults.AuthenticationScheme);
    }

    But cookie is not being cleared. Any ideas on what I’m missing?

  9. Hi Damien,

    Very nice example, i enjoyed learning from it.

  10. noumanbhatti · · Reply

    How to use the return cookie with angular api calls? I couldn’t find it in the code
    I’m using a cross-domain IDP which sets the cookies how to read them in angular so that I can pass it in header with api calls?

  11. Hello @damienbod,
    I haven’t seen any code on the angular side having to deal with Tokens. The way I see it is that, when any of the routes of Angular are hit, you are setting the Request Path to “/” so as to redirect to Home/Index and then get authenticated, etc.

    However, the client side is not aware of any token. What if I want to get the logged in user, etc. How to access this info on the angular app?

    Thanks

  12. Nice post!
    How to handle the router guard in the ‘hybrid’ ?

  13. Hi Damien,
    Very nice example.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.