Securing an ASP.NET Core Razor Pages app with OAuth
Tags: netcore, aspdotnet, oauth, authentication
Published: 2020-05-12 14:58:06

---
title: Securing an ASP.NET Core Razor Pages app with OAuth
published: true
date: 2020-05-06 00:00:00 UTC
tags: netcore,aspdotnet,oauth,authentication
canonical_url: https://fusionauth.io/blog/2020/05/06/securing-asp-netcore-razor-pages-app-with-oauth.html
---

Previously, we used .NET Core to [build a command line tool](https://fusionauth.io/blog/2020/04/28/dot-net-command-line-client) to add users to a FusionAuth user identity management system. In this tutorial, we’ll build out a web application with a protected page. We’ll use Razor Pages and have both login and logout happen against FusionAuth, using the Authorization Code grant.

<!--more-->
## Configuring FusionAuth

You should already have FusionAuth up and running. If you don’t, head over to the previous post “[Creating a user in FusionAuth with a .NET Core CLI Client](https://fusionauth.io/blog/2020/04/28/dot-net-command-line-client)” for instructions on downloading and installing FusionAuth. You’ll also need to create the application. After that, if you want to skip building the CLI client, you can just create a user in the admin user interface.

We are going to make a few changes to the FusionAuth configuration to allow integration with the ASP.Net Core web application.

First, we’re going to create a new signing key. There’s a list of [supported algorithms](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/wiki/Supported-Algorithms) that work with the open source identity project we’re using, but currently [symmetric algorithms are not supported](https://github.com/IdentityModel/IdentityModel.AspNetCore/issues/102)–I love the honesty of the library maintainer: “Don’t use symmetric keys is the easy answer.” Using the default HMAC key resulted in a `SecurityTokenSignatureKeyNotFoundException` and there was no easy way to change the `SignatureProvider`. Luckily, FusionAuth supports [different key types](https://fusionauth.io/docs/v1/tech/apis/keys#generate-a-key), so let’s generate an asymmetric key to use to sign our JSON Web Tokens (JWTs).

To generate the key, go to “Settings” then to “Key Master”. In the upper right hand corner, click on the dropdown next to “Import Public Key” and choose “Generate RSA”. Use a descriptive name like “For dotnetcore” and leave the rest of defaults and click “Submit”.

![A new RSA keypair in FusionAuth.](https://fusionauth.io/assets/img/blogs/authorization-code-grant-asp-net/aspnet-keypair.png)

Next, we’ll be modifying the previously created “dotnetcore”’ application settings. Edit that application, then go to the OAuth tab. Make the following changes:

- Add an Authorized Redirect URL of `http://localhost:5000/signin-oidc`.
- Add a Logout URL of `http://localhost:5000`.

The web application we are going to build is going to be on port 5000, and `signin-oidc` is an endpoint provided by the authentication library. We’re setting things up so that the Authorization Code grant will work. Write down the `Client ID` and `Client Secret`, as we’ll need that information later. When you are done configuring this section, the OAuth tab of your application should look like this:

![The OAuth settings for the 'dotnetcore' application.](https://fusionauth.io/assets/img/blogs/authorization-code-grant-asp-net/aspnet-application-oauth-setup-screen.png)

Next, go to the JWT tab. We need to enable JSON Web Token signing and tell FusionAuth to use the previously generated RSA keypair. Update the “Access token signing key” and “Id token signing key” to point to that keypair. When you are done, the JWT tab should look like this:

![The JWT settings for the 'dotnetcore' application.](https://fusionauth.io/assets/img/blogs/authorization-code-grant-asp-net/aspnet-application-jwt-screen.png)

Click the blue “Save” icon to save all the settings you just configured. We’re done with FusionAuth. (Unless you need to add a user, in which case, go to the “Users” section and add one. Make sure to add them to the “dotnetcore” application by creating the appropriate user registration.)

## Set up a basic ASP.NET Razor Pages application

Now let’s start building our ASP.NET Core web application. We’ll use [Razor Pages](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-3.1&tabs=visual-studio) and ASP.NET Core 3.1. This application will display common information to all users. There will also be a secured area only available to an authenticated user. Good thing we have already added one! As usual, we have the [full source code](https://github.com/FusionAuth/fusionauth-example-asp-netcore) available if you want to download it and take a look.

First, let’s create a new web application using the `dotnet` CLI and go to that directory:

```
dotnet new webapp -o SampleApp
cd SampleApp

```

To see the results, we publish this application and run it. There are [multiple ways of deploying an application](https://docs.microsoft.com/en-us/dotnet/core/deploying/), but I chose publishing to ensure my deployment process was repeatable. In this tutorial, I’ll be building a standalone Windows 64-bit executable, but with the same codebase you can also build a macOS or Linux program. Here’s the command to publish a standalone executable I could deploy behind a proxy like nginx:

```
dotnet publish -r win-x64

```

Then I start up the executable:

```
bin/Debug/netcoreapp3.1/win-x64/publish/SampleApp.exe

```

I can hit `control-C` to exit out of this application. I also like to open up a separate terminal so that I can edit source files (only applicable if you aren’t using Visual Studio or another editor). I also like to combine the publish and start up commands into one line (I run this is in a bash shell):

```
dotnet publish -r win-x64 && bin/Debug/netcoreapp3.1/win-x64/publish/SampleApp.exe

```

If I visit `http://localhost:5000` I’m redirected to an https address and get a warning from my browser. While that might make sense in production, unless we terminated TLS before traffic gets to this application, we don’t need this behavior in development. To avoid this, simply remove `app.UseHttpsRedirection();` from `Startup.cs`, which is the main configuration file for our web application.

We’ll also want to add a page to be secured, which we’ll aptly call “Secure”. Add `Secure.cshtml` and `Secure.cshtml.cs` to the `SampleApp/Pages` directory. Give them the following content:

`Secure.html` should look like his:

```
@page
@model SecureModel
@{
    ViewData["Title"] = "I'm full of secure data";
}
<h1>@ViewData["Title"]</h1>

<p>TBD</p>

```

`Secure.html.cs` should contain this code:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace SampleApp.Pages
{
    public class SecureModel : PageModel
    {
        private readonly ILogger<SecureModel> _logger;

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

        public void OnGet()
        {
        }
    }
}

```

Don’t forget to add a navigation element to `Pages/Shared/_Layout.cshtml` after “Privacy”:

```
...
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Secure">Secure</a>
</li>
...

```

Hit `control-C` to exit the application. Then republish it and start it up again. Visit `http://localhost:5000` and view your new page: click on “Secure”.

If you are looking at the GitHub repository, you can see the application at this point by looking at the [`setup-application` branch](https://github.com/FusionAuth/fusionauth-example-asp-netcore/tree/setup-application).

We’ve added a page, but it sure isn’t secure … yet. Let’s do that next.

## Login

It’s always smart to leverage existing libraries as they are likely to be more secure and better handle edge cases. We’re going to add two new libraries to our application. Make sure you’re in the `SampleApp` directory and run these commands to add them.

```
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
dotnet add package IdentityModel.AspNetCore -v 1.0.0-rc.4.1

```

You’ll note that unlike the [command line tool](https://fusionauth.io/blog/2020/04/28/dot-net-command-line-client), we aren’t using FusionAuth client packages. We’ll be protecting this page using standard OAuth and so there’s no need for any FusionAuth specific API calls. While I’ll be highlighting FusionAuth, you could run this code against any OAuth2 compliant server.

We specify version `1.0.0-rc.4.1` for the [IdentityModel](https://github.com/IdentityModel/IdentityModel.AspNetCore/) package because it hasn’t been released just yet. This package makes integrating with standards-based OAuth servers a snap, so we’ll overlook it being not quite released.

We need to protect our “Secure” page. We do this using the [Authorize filter attribute](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/filter?view=aspnetcore-3.1#authorize-filter-attribute) on the backing class. From `Secure.cshtml.cs`:

```
// ...
namespace SampleApp.Pages
{
    [Authorize]
    public class SecureModel : PageModel
    {
// ...

```

We could also have added a `RazorPagesOptions` to our `Startup.cs` file, specifying which pages should be protected by an authorization call. This would be a good choice if we wanted everything centralized. Depending on the needs and size of your application, one of these choices might be better than the other.

```
// ...
services.AddRazorPages().AddRazorPagesOptions(options =>
                 {
                     options.Conventions.AuthorizePage("/Secure");
                 });
// ...

```

We’ll also display the claims contained in the JWT that FusionAuth creates upon authentication. Here `Secure.cshtml` iterates over the claims; update that file to include the text after and including `Claims`. A claim is essentially the information the authentication server has shared about a subject in the JWT.

```
@page
@using Microsoft.AspNetCore.Authentication
@model SecureModel
@{
    ViewData["Title"] = "I'm full of secure data";
}
<h1>@ViewData["Title"]</h1>

<h2>Claims</h2>

<dl>
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>
    }
</dl>

```

We also need to set up some services to specify how this page is protected. Here’s the full `Startup.cs` code:

```
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Logging;

namespace SampleApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
            services.AddRazorPages();

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "cookie";
                options.DefaultChallengeScheme = "oidc";
            })
                .AddCookie("cookie", options =>
                {
                    options.Cookie.Name = "mycookie";

                    options.Events.OnSigningOut = async e =>
                    {
                        await e.HttpContext.RevokeUserRefreshTokenAsync();
                    };
                })
                .AddOpenIdConnect("oidc", options =>
                {
                    options.Authority = Configuration["SampleApp:Authority"];

                    options.ClientId = Configuration["SampleApp:ClientId"];
                    options.ClientSecret = Configuration["SampleApp:ClientSecret"];

                    options.ResponseType = "code";
                    options.RequireHttpsMetadata = false;
                });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/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();
            }

            app.UseStaticFiles();

            app.UseRouting();

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

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

```

Let’s go through some of the more interesting parts. First, we’re setting up our authentication including the scheme and challenge method. We’ll be using cookies to store our authentication information and “oidc” for our authentication provider, which is defined further below.

```
// ...
services.AddAuthentication(options =>
{
    options.DefaultScheme = "cookie";
    options.DefaultChallengeScheme = "oidc";
})
// ...

```

Here we configure the cookie, including setting the cookie name:

```
// ...
.AddCookie("cookie", options =>
{
    options.Cookie.Name = "mycookie";
// ...

```

Finally, we set up our previously referenced authentication provider, `"oidc"`. You could have multiple providers. We create an [OpenIdConnectOptions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions?view=aspnetcore-3.1) object to fully configure this provider. Setting `ResponseType = "code"` is what forces the use of the Authorization Code grant. PKCE is turned on by default. We pull configuration information like our client id from either `appsettings.json` or the environment. These are the values you saved off when you were configuring FusionAuth. (We’ll add them to `appsettings.json` a bit later.) We create an [OpenIdConnectOptions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions?view=aspnetcore-3.1) object to configure our provider. Since we want to use the Authorization Code grant, we set `ResponseType = "code"`. PKCE is turned on by default, so we’re ready for [OAuth 2.1](https://fusionauth.io/blog/2020/04/15/whats-new-in-oauth-2-1).

```
// ...
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = Configuration["SampleApp:Authority"];

    options.ClientId = Configuration["SampleApp:ClientId"];
    options.ClientSecret = Configuration["SampleApp:ClientSecret"];

    options.ResponseType = "code";
    options.RequireHttpsMetadata = false;
});
// ...

```

We also need to turn on authentication for our application:

```
// ...
app.UseAuthentication();
// ...

```

Wait, I thought we were preventing users from accessing certain pages? Isn’t that authorization, not authentication? When we first set up the application, we didn’t have any authentication scheme configured. And, in this case, we’re actually prohibiting access to any anonymous user, so any authenticated user is authorized.

For debugging, add `IdentityModelEventSource.ShowPII = true;` to the very end of `Configure` method. This makes it easier to see [errors in the OAuth flow](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/wiki/PII). But for production code, please remove it.

```
// ...
IdentityModelEventSource.ShowPII = true;
// ...

```

As promised, here’s our `appsettings.json` file. We need to add our entire `SampleApp` object so that the code above can be configured correctly. Authority is just the location of the user identity server, in this case FusionAuth.

```
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "SampleApp" : {
      "Authority" : "http://localhost:9011",
      "ClientId" : "4420013f-bc5e-4d5a-9f94-f4b64ad5107c"
   }
}

```

Wait, where’s the client secret? This file is in git, but we should not put secrets under version control. Instead, the client secret is provided on the command line via an environment variable. This change means the correct way to publish and start the web application is now (where you replace `H4...` with the client secret value):

```
dotnet publish -r win-x64 && SampleApp__ClientSecret=H4... bin/Debug/netcoreapp3.1/win-x64/publish/SampleApp.exe

```

Once you’ve updated all these files, you can publish and start the application. You should be able to log in with a previously created user and see the claims. Go to `http://localhost:5000` and click on the “Secure” page. You’ll be prompted to log in using FusionAuth’s default login page. You can [theme the login screen of FusionAuth](https://fusionauth.io/docs/v1/tech/themes/) if you want to make the login page look like your company’s brand.

![The login screen in FusionAuth.](https://fusionauth.io/assets/img/blogs/authorization-code-grant-asp-net/login-asp-dot-net-example.png)

After you’ve signed in, you’ll end up at the “Secure” page and will see all claims encoded in the JWT.

![The secure page.](https://fusionauth.io/assets/img/blogs/authorization-code-grant-asp-net//successful-login-secure-page-display.png)

You can see the source of the application at this stage of development by looking at the [`add-authentication` branch](https://github.com/FusionAuth/fusionauth-example-asp-netcore/tree/add-authentication).

## Logout

Awesome, now you can log in with valid user credentials. However, right now there’s no way to log out. The JWT is stored in a session cookie. When we’re ready to leave, we want to log out of our ASP.NET Core session and of the FusionAuth session. So, we need to add a logout page, remove the session cookie, and redirect to the FusionAuth OAuth logout endpoint. FusionAuth will destroy its session and then redirect back to the configured `Logout URL`. We’ll add a Logout page to do all of this.

Here’s what the `Logout.cshtml.cs` class looks like:

```
namespace SampleApp.Pages
{
    public class LogoutModel : PageModel
    {
        private readonly ILogger<LogoutModel> _logger;
        private readonly IConfiguration _configuration;

        public LogoutModel(ILogger<LogoutModel> logger, IConfiguration configuration)
        {
            _logger = logger;
            _configuration = configuration;
        }

        public IActionResult OnGet()
        {
              SignOut("cookie", "oidc");
              var host = _configuration["SampleApp:Authority"];
              var cookieName = _configuration["SampleApp:CookieName"];

              var clientId = _configuration["SampleApp:ClientId"];
              var url = host + "/oauth2/logout?client_id="+clientId;
              Response.Cookies.Delete(cookieName);
              return Redirect(url);
        }
    }
}

```

`OnGet` is the important method. Here we sign out using a method of our authentication library, delete the JWT cookie and send the user to the FusionAuth OAuth logout endpoint. Don’t forget to add the `Logout` page to the navigation, but only if the user is signed in:

```
...
@if (User.Identity.IsAuthenticated)
{
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-page="/Logout">Logout</a>
    </li>
}
...

```

You also need to update the `appsettings.json` file with the cookie name setting. Since we’re now referencing the cookie in two places, pulling it out to the `appsettings.json` file will make for a more maintainable application.

```
{
  ...
  "SampleApp" : {
    "Authority" : "http://localhost:9011",
    "CookieName" : "mycookie",
    "ClientId" : "4420013f-bc5e-4d5a-9f94-f4b64ad5107c"
  },
  ...
}

```

Finally, we need to change the `Startup.cs` file to use the new cookie name.

```
// ...
.AddCookie("cookie", options =>
{
    options.Cookie.Name = Configuration["SampleApp:CookieName"];
})
// ...

```

Great! Now you can both sign in and sign out of your application.

(If you are interested, you can see the application’s current code by looking at the [`add-logout` branch](https://github.com/FusionAuth/fusionauth-example-asp-netcore/tree/add-logout).

## Next steps

If you want to explore more, you can add more pages to the application, limit a page to a certain user, or customize the FusionAuth login page theme.

You could also welcome users with their first name by retrieving user information via an API call, using the [FusionAuth .NET Core client library](https://fusionauth.io/docs/v1/tech/client-libraries/netcore).

For the next post in this .NET Core series, we’ll be associating a user with a role and creating a custom claim in the JWT, so stay tuned!