Protecting your Serverless .NET Core App with Amazon Cognito

This blog post focuses on implementing an ASP.NET Core MVC web application using OpenID Connect, Amazon Cognito and AWS Lambda.

Introduction

Most business applications require some form of authentication and authorisation. It can be a challenging component to implement, especially handling sensitive data like profile information, passwords, federated identities.

Amazon Cognito is a fantastic AWS service that handles all the undifferentiated heavy lifting to do with your app’s users, so you don’t have to. You don’t have to worry about authentication, user profiles, user management, or ensuring that passwords are handled properly. AWS Cognito also handles federation so you can authenticate users through social identity providers such as Facebook, Twitter, or Amazon; with SAML identity solutions; or by using your own identity system.

If you’ve previously implemented .NET MVC apps with authentication, and you’re looking to implement authentication in a Serverless environment, you’ve come to the right place! There have been many, many iterations of the authentication and authorization layer over the years for ASP.NET MVC. With ASP.NET Core, Microsoft have simplified the request pipeline, allowing you to plug in functionality if required - referred to as ASP.NET Core Middleware.

This blog post focuses on leveraging Open ID Connect, however, you can alternatively use the AWS .NET SDK to directly integrate with Amazon Cognito. See this great blog post by Tom Moore & Mike Morain.

Install Pre-requisites

Microsoft and .NET are a first class citizens on AWS. The best part about Visual Studio with the AWS Toolkit is the extensive integration with AWS services. You can use the same Visual Studio Solution without any complex configuration or additional effort to develop locally and on AWS Lambda. This allows your project to get off the ground fast, and innovate rapidly!

To get started, install the following:

  1. Install Visual Studio 2017: https://visualstudio.microsoft.com/.
  2. Install the AWS Toolkit for Visual Studio 2017: https://aws.amazon.com/visualstudio/.

This is a handy User Guide for setting up the AWS Toolkit.

Start a new Serverless Web App Project

To get started, simply make a new AWS Serverless Project and choose ASP.NET Core Web App Blueprint. You’ll see that a base project is set up for you automatically.

my alt text
Figure 1. ASP.NET Core Web App Blueprint

You can start developing straight away. When you’re ready to test, click the Visual Studio Run button and a local IIS Express web server will run your application.

Ready to deploy? Right Click on the Project -> Choose Publish to AWS Lambda.

It’s really that simple to get a .NET Core ASP.NET app running serverless!

Now that the Serverless application is deployed and accessible, I’ve chosen to use a couple of other AWS Services to customise the web application for this tutorial.

  • A public SSL certificate has been generated in the us-east-1 region using the AWS Certificate Manager.
  • A custom domain has been added to my deployed API Gateway. I’ve specified that the custom domain will be directed to our Prod stage for our API Gateway.
  • A new Route53 Hosted Zone was created to provide DNS for my web application. A www record has been created in the Route 53 Hosted Zone with an Alias pointing to the API Gateway Custom Domain CloudFront distribution.
my alt text
Figure 1. API Gateway Custom Domain added, with an SSL certificate
my alt text
Figure 2. Route 53 Domain created and an Alias is configured to point to the CloudFront distribution of the API Gateway custom domain

Set up AWS Cognito

Open the AWS Console, and choose Amazon Cognito. Make a New User Pool. Step through the settings and customise the configuration as required. Click “Create Pool”. Take note of the UserPool Id, as we’ll use that in the next steps.

my alt text
Figure 1. Take note of the User Pool Id

Under App Integration from the menu, choose Domain Name. Specify an available domain name to use. If you don’t specify a domain name, you won’t be able to use the built in Amazon Cognito sign-in and sign-up pages. If you’d like to use your own custom domain, ensure that you’ve set up an Amazon Cognito domain first before specifying your own custom domain.

my alt text
Figure 2. Specify an AWS Cognito domain to use for the Amazon Cognito hosted authentication web interface

An App Client represents an Application that will be configured to interact with Amazon Cognito. Click on App Clients, specify an App Client name and click Create app client. On the subsequent page, you will be provided with an “App client id” and an “App client secret”. Take note of these values, as they will be used in the next steps.

my alt text
Figure 3. Create a new App Client and take note of the Client Id and Client Secret values

Next, we will set up the Callback URL and Sign Out URL to integrate Amazon Cognito with our app.

The Open ID Connect middleware implements a special route to /signin-oidc which will handle the Open ID Connect process flow in our ASP.NET Core app. The Callback URL should be the special /signin-oidc path.

The Sign Out URL should be a URL of a protected resource, so that your application will automatically redirect to Amazon Cognito.

my alt text
Figure 4. Specify the Open ID Connect middleware endpoint for Callback URL and a Sign out URL.

Set up ASP.NET Core Authentication Middleware with Amazon Cognito

In the Startup.cs class, we’ll include the ASP.NET Authentication Middleware, by specifying app.UseAuthentication() in the Configure method and specifying Amazon Cognito using Open ID Connect configuration settings.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }
    app.UseAuthentication();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();
    app.UseMvc(routes => {
        routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}

public void ConfigureServices(IServiceCollection services)
{
    var clientSecret = Configuration["AmazonCognito:ClientSecret"];
    var clientId = Configuration["AmazonCognito:ClientId"];
    var metadataAddress = Configuration["AmazonCognito:MetaDataAddress"];
    var logOutUrl = Configuration["AmazonCognito:LogOutUrl"];
    var baseUrl = Configuration["AmazonCognito:BaseUrl"];
    
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie()
    .AddOpenIdConnect(options =>
    {
        options.ResponseType = "code";
        options.MetadataAddress = metadataAddress;
        options.ClientId = clientId;
        options.ClientSecret = clientSecret;
        options.GetClaimsFromUserInfoEndpoint = true;
        options.Scope.Add("email");
        options.Events = new OpenIdConnectEvents
        {
            OnRedirectToIdentityProviderForSignOut = (context) =>
            {
                var logoutUri = logOutUrl;
                logoutUri += $"?client_id={clientId}&logout_uri={baseUrl}";
                context.Response.Redirect(logoutUri);
                context.HandleResponse();
                return Task.CompletedTask;
            }
        };
    });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

In the appsettings.json file, I’ve included an AmazonCognito configuration section, so that I can easily swap out configuration for my different production, testing and development environments.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AmazonCognito": {
    "ClientSecret": "**COGNITO_CLIENT_SECRET**",
    "ClientId": "**COGNITO_CLIENT_ID**",
    "MetaDataAddress": "https://cognito-idp.ap-southeast-2.amazonaws.com/*USERPOOL_ID*/.well-known/openid-configuration",
    "LogOutUrl": "https://apsnetcore-serverless-auth-demo.auth.ap-southeast-2.amazoncognito.com/logout",
    "BaseUrl": "https://www.sparktail.com.au/"
  }
}

To require authentication for certain resources, we can now simply decorate these methods or classes with the [Authorize] attribute.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace AWSLambdaCognitoOpenIDConnect.Controllers
{
    public class HomeController : Controller
    {
        [Authorize]
        public IActionResult Index()
        {
            return View();
        }
    }
}

That’s it! The application is now protected and requires authentication for access.

Browsing to the Home page will redirect us to the Amazon Cognito hosted interface.

my alt text
Figure 5. Redirection to the Hosted Amazon Cognito interface.

You can now easily set up federation and integration with other identity systems, all from Amazon Cognito.

Using SAML-enabled IdP with Amazon Cognito, ASP.NET Core and API Gateway

It is a common scenario that businesses want to protect their web applications with a corporate identity. Azure Active Directory is a common idenity provider that provides authentication and authorisation in the enterprise. It’s easy to set up federation with Amazon Cognito using SAML.

In a serverless context, there are some additional considerations. SAML is generally quite loquacious. There is a lot of information that is passed back and forth through HTTP headers. AWS API Gateway has a hard limit for HTTP headers of 10KB in size. HTTP Header length is variable and can occasionally be greater than the 10KB limit, which can result in AWS API Gateway returning a ‘Bad Request’ error, or a 404 error like below.

my alt text
Error when API gateway request includes headers of more than 10kb

To work around this issue, we can filter out the non-essential headers. To do this, we can extend the Sign-In event, which is available in the .AddCookie() options.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
...
services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options => options.Events.OnSigningIn = FilterGroupClaims)
.AddOpenIdConnect(options =>
{
    options.ResponseType = "code";
    options.MetadataAddress = metadataAddress;
    options.ClientId = clientId;
...

We can add helper methods to filter out the claims.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private static Task FilterGroupClaims(CookieSigningInContext context)
{
    var principal = context.Principal;
    if (principal.Identity is ClaimsIdentity identity)
    {
        var unused = identity.FindAll(GroupsToRemove).ToList();
        unused.ForEach(c => identity.TryRemoveClaim(c));
    }
    return Task.FromResult(principal);
}
private static bool GroupsToRemove(Claim claim)
{
    string[] _groupObjectIds = new string[] { "identities" };
    return claim.Type == "groups" && !_groupObjectIds.Contains(claim.Type);
}

Now you’ll be be able to successfully login to your serverless application through Amazon Cognito, using Azure AD.

Should Authentication be implemented on the API Gateway layer, or within the ASP.NET Core Application?

There is no one ‘correct’ solution. It all depends on your application, preferences and tooling.

It is possible to implement authentication and authorization using Custom Authorizers on the API Gateway. However, in my opinion, one great benefit of ASP.NET MVC is the ability to simplify your application authentication and authorization logic using built in functionality and method decorators. I also think that being able to unit test your entire application with the standard Microsoft unit testing framework is quite good.

There are considerations for all approaches. You can decide what is the right solution for your application.

The source code to get started

Source code used in this blog post has been made available here.

Conclusion

In this post, we went through how to set up an ASP.NET Core Serverless App, with authentication provided by Amazon Cognito. After setting up the user pool and configuring our ASP.NET Core web application using the ASP.NET Middleware, we added method decorators to protect our application.