Securing an ASP.NET Core Razor Page App using OpenID Connect Code flow with PKCE

This article shows how to secure an ASP.NET Core Razor Page application using the Open ID Connect code flow with PKCE (Proof Key for Code Exchange). The secure token server is implemented using IdentityServer4 but any STS could be used which supports PKCE.

Code: https://github.com/damienbod/AspNetCoreHybridFlowWithApi

History

2020-12-11 Updated to .NET 5

An ASP.NET Core 5 Razor Page application without identity was created using the Visual Studio templates. The Microsoft.AspNetCore.Authentication.OpenIdConnect Nuget package was then added to the project.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.1" />
  </ItemGroup>

</Project>

In the startup class, the ConfigureServices method is used to add the authentication and the authorization. Cookies is used to persist the session, if authorized, and OpenID Connect is used to signin, signout. If a new session is started, the application redirects to IdentityServer4 and secures both the identity and the application using the OpenID Connect code flow with PKCE (Proof key for code exchange). Both the PKCE and the secret are required.

public void ConfigureServices(IServiceCollection services)
{
	services.AddAuthentication(options =>
	{
		options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
		options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
	})
   .AddCookie()
   .AddOpenIdConnect(options =>
   {
	   options.SignInScheme = "Cookies";
	   options.Authority = "https://localhost:44352";
	   options.RequireHttpsMetadata = true;
	   options.ClientId = "codeflowpkceclient";
	   options.ClientSecret = "codeflow_pkce_client_secret";
	   options.ResponseType = "code";
	   options.UsePkce = true;
	   options.Scope.Add("profile");
	   options.Scope.Add("offline_access");
	   options.SaveTokens = true;
   });

   services.AddAuthorization();
   services.AddRazorPages();
}

The Configure method adds the middleware so that the authorization is used. Both the UseAuthentication() and UseAuthorization() methods are required, and must be added after the AddRouting() method.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	// code not needed for example
	app.UseStaticFiles();

	app.UseRouting();

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

	app.UseEndpoints(endpoints =>
	{
		endpoints.MapRazorPages();
	});
}

IdentityServer4 is configured to accept the client configuration from above. Both the PKCE and the secret are required. The configuration must match the client configuration exactly. In a production application, the secrets must be removed from the code and read from a safe configuration like for example Azure key vault. The URLs would also be read from app.settings or something like this.

new Client
{
	ClientName = "codeflowpkceclient",
	ClientId = "codeflowpkceclient",
	ClientSecrets = {new Secret("codeflow_pkce_client_secret".Sha256()) },
	AllowedGrantTypes = GrantTypes.Code,
	RequirePkce = true,
	RequireClientSecret = true,
	AllowOfflineAccess = true,
	AlwaysSendClientClaims = true,
	UpdateAccessTokenClaimsOnRefresh = true,
	AlwaysIncludeUserClaimsInIdToken = true,
	RedirectUris = {
		"https://localhost:44330/signin-oidc",
	},
	PostLogoutRedirectUris = {
		"https://localhost:44330/signout-callback-oidc",
	},
	AllowedScopes = new List<string>
	{
		IdentityServerConstants.StandardScopes.OpenId,
		IdentityServerConstants.StandardScopes.Profile,
		IdentityServerConstants.StandardScopes.OfflineAccess
	}
}

The Authorize attribute needs to be added to all pages which are to be secured. You could also require that the whole application is to be secure and opt out for the non-secure pages. If the page is called in a browser, the application will automatically redirect the user, application to authenticate.

[Authorize]
public class IndexModel : PageModel
{
	private readonly ILogger<IndexModel> _logger;

	public IndexModel(ILogger<IndexModel> logger)
	{
		_logger = logger;
	}

	public void OnGet()
	{

	}
}

The application also needs a signout. This is implemented using two new pages, a logout page, and a SignedOut page. If the user clicks the logout link, the application removes the session and redirects to a public page of the application.

[Authorize]
public class LogoutModel : PageModel
{
	public async Task<IActionResult> OnGetAsync()
	{
		await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

		return Redirect("/SignedOut");
	}
}

Now the Razor page application, identity can signin, signout using OpenID Connect Code Flow with PKCE and also uses a secret to authorize the client.

Links:

https://openid.net/specs/openid-connect-core-1_0.html

https://docs.microsoft.com/en-us/aspnet/core/security/?view=aspnetcore-3.0

https://tools.ietf.org/html/rfc7636

https://docs.microsoft.com/en-us/aspnet/core/razor-pages

8 comments

  1. You don’t need a client secret when using PKCE. In fact, that’s the whole point of PKCE and why it’s recommended for public clients that can’t safely store the client secret. But a server web app is a confidential client, so the client secret is safe anyway, and you don’t need PKCE (although it can’t hurt to use it, of course).

    1. Hi Thomas yes you do need a secret if you want to authenticate the client, this way, only a client hosted by me can be used as the client. This is not a public client.

      Greetings Damien

      1. And the PKCE is required due to code substitution attacks or you could use the Hybrid flow which also protects against this. Learnt this on twitter…

        Cut and pasted code attack in OAuth 2.0 [RFC6749]

      2. Ah, I see. That makes sense, thanks for the explanation!

      3. Thanks for the feedback

  2. […] Securing an ASP.NET Core Razor Page App using OpenID Connect Code flow with PKCE – Damien Bowden […]

  3. […] Securing an ASP.NET Core Razor Page App using OpenID Connect Code flow with PKCE (Damien Bowden) […]

  4. Deivydas · · Reply

    Hi, is IdentityServer4 and its configuration mandatory here?

Leave a comment

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